From adeb976a1bc693b75dc44423cdaa8ac88c1f5a31 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 6 Oct 2014 11:53:52 +0530 Subject: [PATCH] Block negative stock in perpetual inventory --- .../accounts_settings/accounts_settings.py | 3 ++ .../doctype/sales_invoice/sales_invoice.py | 14 +++--- erpnext/accounts/general_ledger.py | 3 +- erpnext/controllers/stock_controller.py | 50 +++++++++++++++---- .../stock_reconciliation.py | 4 +- .../doctype/stock_settings/stock_settings.py | 14 ++++-- erpnext/stock/stock_ledger.py | 4 +- 7 files changed, 63 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index f0890dd439..7280322a68 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -14,6 +14,9 @@ class AccountsSettings(Document): frappe.db.set_default("auto_accounting_for_stock", self.auto_accounting_for_stock) if cint(self.auto_accounting_for_stock): + if cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")): + frappe.throw(_("Negative stock is not allowed in case of Perpetual Inventory, please disable it from Stock Settings")) + # set default perpetual account in company for company in frappe.db.sql("select name from tabCompany"): frappe.get_doc("Company", company[0]).save() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a2bf78c449..31f7113c37 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -7,7 +7,7 @@ import frappe.defaults from frappe.utils import cint, cstr, flt from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date -from erpnext.controllers.stock_controller import update_gl_entries_after +from erpnext.controllers.stock_controller import update_gl_entries_after, block_negative_stock from frappe.model.mapper import get_mapped_doc from erpnext.controllers.selling_controller import SellingController @@ -456,8 +456,8 @@ class SalesInvoice(SellingController): self.make_sl_entries(sl_entries) - def make_gl_entries(self, repost_future_gle=True): - gl_entries = self.get_gl_entries() + def make_gl_entries(self, repost_future_gle=True, allow_negative_stock=False): + gl_entries = self.get_gl_entries(allow_negative_stock=allow_negative_stock) if gl_entries: from erpnext.accounts.general_ledger import make_gl_entries @@ -476,7 +476,7 @@ class SalesInvoice(SellingController): items, warehouses = self.get_items_and_warehouses() update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items) - def get_gl_entries(self, warehouse_account=None): + def get_gl_entries(self, warehouse_account=None, allow_negative_stock=False): from erpnext.accounts.general_ledger import merge_similar_entries gl_entries = [] @@ -485,7 +485,7 @@ class SalesInvoice(SellingController): self.make_tax_gl_entries(gl_entries) - self.make_item_gl_entries(gl_entries) + self.make_item_gl_entries(gl_entries, allow_negative_stock) # merge gl entries before adding pos entries gl_entries = merge_similar_entries(gl_entries) @@ -520,7 +520,7 @@ class SalesInvoice(SellingController): }) ) - def make_item_gl_entries(self, gl_entries): + def make_item_gl_entries(self, gl_entries, allow_negative_stock=False): # income account gl entries for item in self.get("entries"): if flt(item.base_amount): @@ -537,7 +537,7 @@ class SalesInvoice(SellingController): # expense account gl entries if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) \ and cint(self.update_stock): - gl_entries += super(SalesInvoice, self).get_gl_entries() + gl_entries += super(SalesInvoice, self).get_gl_entries(allow_negative_stock=allow_negative_stock) def make_pos_gl_entries(self, gl_entries): if cint(self.is_pos) and self.cash_bank_account and self.paid_amount: diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 211476822f..073ef8a5af 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -97,8 +97,7 @@ def validate_account_for_auto_accounting_for_stock(gl_map): for entry in gl_map: if entry.account in aii_accounts: - frappe.throw(_("Account: {0} can only be updated via \ - Stock Transactions").format(entry.account), StockAccountInvalidTransaction) + frappe.throw(_("Account: {0} can only be updated via Stock Transactions").format(entry.account), StockAccountInvalidTransaction) def delete_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 575525399f..304ded2390 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -8,10 +8,10 @@ from frappe import msgprint, _ import frappe.defaults from erpnext.controllers.accounts_controller import AccountsController -from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries +from erpnext.accounts.general_ledger import make_gl_entries, delete_gl_entries, process_gl_map class StockController(AccountsController): - def make_gl_entries(self, repost_future_gle=True): + def make_gl_entries(self, repost_future_gle=True, allow_negative_stock=False): if self.docstatus == 2: delete_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -19,16 +19,19 @@ class StockController(AccountsController): warehouse_account = get_warehouse_account() if self.docstatus==1: - gl_entries = self.get_gl_entries(warehouse_account) + gl_entries = self.get_gl_entries(warehouse_account, allow_negative_stock) make_gl_entries(gl_entries) if repost_future_gle: items, warehouses = self.get_items_and_warehouses() - update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items, warehouse_account) + update_gl_entries_after(self.posting_date, self.posting_time, warehouses, items, + warehouse_account, allow_negative_stock) def get_gl_entries(self, warehouse_account=None, default_expense_account=None, - default_cost_center=None): - from erpnext.accounts.general_ledger import process_gl_map + default_cost_center=None, allow_negative_stock=False): + + block_negative_stock(allow_negative_stock) + if not warehouse_account: warehouse_account = get_warehouse_account() @@ -46,12 +49,17 @@ class StockController(AccountsController): self.check_expense_account(detail) + stock_value_difference = flt(sle.stock_value_difference, 2) + if not stock_value_difference: + valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, sle.posting_date) + stock_value_difference = flt(sle.qty)*flt(valuation_rate) + gl_list.append(self.get_gl_dict({ "account": warehouse_account[sle.warehouse], "against": detail.expense_account, "cost_center": detail.cost_center, "remarks": self.get("remarks") or "Accounting Entry for Stock", - "debit": flt(sle.stock_value_difference, 2) + "debit": stock_value_difference })) # to target warehouse / expense account @@ -60,7 +68,7 @@ class StockController(AccountsController): "against": warehouse_account[sle.warehouse], "cost_center": detail.cost_center, "remarks": self.get("remarks") or "Accounting Entry for Stock", - "credit": flt(sle.stock_value_difference, 2) + "credit": stock_value_difference })) elif sle.warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(sle.warehouse) @@ -214,7 +222,8 @@ class StockController(AccountsController): return serialized_items -def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, warehouse_account=None): +def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, + warehouse_account=None, allow_negative_stock=False): def _delete_gl_entries(voucher_type, voucher_no): frappe.db.sql("""delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) @@ -228,12 +237,12 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for for voucher_type, voucher_no in future_stock_vouchers: existing_gle = gle.get((voucher_type, voucher_no), []) voucher_obj = frappe.get_doc(voucher_type, voucher_no) - expected_gle = voucher_obj.get_gl_entries(warehouse_account) + expected_gle = voucher_obj.get_gl_entries(warehouse_account, allow_negative_stock) if expected_gle: if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): _delete_gl_entries(voucher_type, voucher_no) - voucher_obj.make_gl_entries(repost_future_gle=False) + voucher_obj.make_gl_entries(repost_future_gle=False, allow_negative_stock=allow_negative_stock) else: _delete_gl_entries(voucher_type, voucher_no) @@ -285,3 +294,22 @@ def get_warehouse_account(): warehouse_account = dict(frappe.db.sql("""select master_name, name from tabAccount where account_type = 'Warehouse' and ifnull(master_name, '') != ''""")) return warehouse_account + +def block_negative_stock(allow_negative_stock=False): + if cint(frappe.defaults.get_global_default("auto_accounting_for_stock")) and not allow_negative_stock: + if cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")): + frappe.throw(_("Negative stock is not allowed in case of Perpetual Inventory, please disable it from Stock Settings")) + +def get_valuation_rate(item_code, warehouse, posting_date): + last_valuation_rate = frappe.db.sql("""select valuation_rate + from `tabStock Ledger Entry` + where item_code = %s and warehouse = %s + and ifnull(qty_after_transaction, 0) > 0 and posting_date < %s + order by posting_date desc limit 1""", (item_code, warehouse, posting_date)) + + valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 + + if not valuation_rate: + valuation_rate = frappe.db.get_value("Item Price", {"item_code": item_code, "buying": 1}, "price") + + return valuation_rate diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 2aa9ab61c6..8e837e275c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -203,12 +203,12 @@ class StockReconciliation(StockController): "posting_time": self.posting_time }) - def get_gl_entries(self, warehouse_account=None): + def get_gl_entries(self, warehouse_account=None, allow_negative_stock=False): if not self.cost_center: msgprint(_("Please enter Cost Center"), raise_exception=1) return super(StockReconciliation, self).get_gl_entries(warehouse_account, - self.expense_account, self.cost_center) + self.expense_account, self.cost_center, allow_negative_stock=allow_negative_stock) def validate_expense_account(self): if not cint(frappe.defaults.get_global_default("auto_accounting_for_stock")): diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index b505394f1b..95ace86b79 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -6,18 +6,20 @@ from __future__ import unicode_literals import frappe from frappe import _ - +from frappe.utils import cint from frappe.model.document import Document class StockSettings(Document): def validate(self): - for key in ["item_naming_by", "item_group", "stock_uom", - "allow_negative_stock"]: + if cint(self.allow_negative_stock) and cint(frappe.defaults.get_global_default("auto_accounting_for_stock")): + frappe.throw(_("Negative stock is not allowed in case of Perpetual Inventory")) + + for key in ["item_naming_by", "item_group", "stock_uom", "allow_negative_stock"]: frappe.db.set_default(key, self.get(key, "")) - + from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series - set_by_naming_series("Item", "item_code", + set_by_naming_series("Item", "item_code", self.get("item_naming_by")=="Naming Series", hide_name_field=True) stock_frozen_limit = 356 @@ -25,3 +27,5 @@ class StockSettings(Document): if submitted_stock_frozen > stock_frozen_limit: self.stock_frozen_upto_days = stock_frozen_limit frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit) + + diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b7c2074003..e8a84c2ab1 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -36,9 +36,9 @@ def make_sl_entries(sl_entries, is_amended=None): "is_amended": is_amended }) update_bin(args) + if cancel: - delete_cancelled_entry(sl_entries[0].get('voucher_type'), - sl_entries[0].get('voucher_no')) + delete_cancelled_entry(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) def set_as_cancel(voucher_type, voucher_no): frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled='Yes',