diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index 39ea4cb329d..c2ea54204f6 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -57,6 +57,68 @@ class TestBin(ERPNextTestSuite): self.assertEqual(bin.valuation_rate, 0) self.assertEqual(bin.stock_value, 0) + def test_repost_resets_bin_without_sle(self): + """A repost must zero the bin when the ledger is empty, e.g. after entries were deleted.""" + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + from erpnext.stock.stock_ledger import update_entries_after + + item_code = make_item().name + warehouse = "_Test Warehouse - _TC" + make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100) + + # deleting a transaction with `delete_linked_ledger_entries` on drops its entries outright + frappe.db.delete("Stock Ledger Entry", {"item_code": item_code, "warehouse": warehouse}) + + update_entries_after( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": "1900-01-01", + "posting_time": "00:01", + } + ) + + bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse}) + self.assertEqual(bin.actual_qty, 0) + self.assertEqual(bin.valuation_rate, 0) + self.assertEqual(bin.stock_value, 0) + + def test_cancelling_last_entry_resets_bin(self): + """Cancelling the only voucher must clear stock value, not just quantity.""" + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item_code = make_item().name + warehouse = "_Test Warehouse - _TC" + se = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100) + + se.cancel() + + bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse}) + self.assertEqual(bin.actual_qty, 0) + self.assertEqual(bin.valuation_rate, 0) + self.assertEqual(bin.stock_value, 0) + + def test_deleting_last_voucher_resets_bin(self): + """Deleting the only voucher wipes its ledger entries outright, the bin must still be cleared.""" + from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + + item_code = make_item().name + warehouse = "_Test Warehouse - _TC" + delete_entries = frappe.get_single_value("Accounts Settings", "delete_linked_ledger_entries") + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 1) + + try: + se = make_stock_entry(item_code=item_code, target=warehouse, qty=10, rate=100) + se.cancel() + frappe.delete_doc("Stock Entry", se.name, force=1) + finally: + frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", delete_entries) + + bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse}) + self.assertEqual(bin.actual_qty, 0) + self.assertEqual(bin.valuation_rate, 0) + self.assertEqual(bin.stock_value, 0) + def test_index_exists(self): # has_index is db-agnostic; raw "SHOW INDEX" is MySQL-only and errors on Postgres if not frappe.db.has_index("tabBin", "unique_item_warehouse"): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index cc192d4dba9..4c9c2c386ca 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -714,16 +714,6 @@ class update_entries_after: previous_sle = get_previous_sle_of_current_voucher(args) if previous_sle: self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = previous_sle - else: - self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = frappe._dict( - { - "qty_after_transaction": 0.0, - "valuation_rate": 0.0, - "stock_value": 0.0, - "prev_stock_value": 0.0, - "stock_queue": [], - } - ) warehouse_dict.previous_sle = previous_sle @@ -1951,6 +1941,30 @@ class update_entries_after: frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True) + self.reset_bin_without_stock_ledger_entries() + + def reset_bin_without_stock_ledger_entries(self): + """Reset the bin when its ledger has no entries left, prev_sle_dict never covers that case.""" + item_code, warehouse = self.args.get("item_code"), self.args.get("warehouse") + if not item_code or not warehouse or (item_code, warehouse) in self.prev_sle_dict: + return + + if frappe.db.exists( + "Stock Ledger Entry", {"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0} + ): + return + + bin_name = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}) + if not bin_name: + return + + frappe.db.set_value( + "Bin", + bin_name, + {"actual_qty": 0.0, "stock_value": 0.0, "valuation_rate": 0.0}, + update_modified=True, + ) + def get_sle_against_current_voucher(kwargs): kwargs["posting_datetime"] = get_combine_datetime(kwargs.posting_date, kwargs.posting_time)