mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-26 13:25:20 +00:00
fix(stock): reset bin when no stock ledger entries remain (#58362)
* fix(stock): reset bin when no stock ledger entries remain update_bin() only writes bins reachable through prev_sle_dict, and that dict is empty once the last live sle for an item and warehouse is cancelled or deleted. actual_qty is still recomputed, but stock_value and valuation_rate stay stale and a repost cannot heal them, so bin totals drift permanently from the stock balance. zero those bins after the normal update, guarded by a re-check that no live sle exists. also drop the prev_sle_dict seeding added earlier in initialize_previous_data, which never took effect because initialize_reposting() discards the dict before update_bin() reads it. * test(stock): cover bin reset when ledger is empty three cases that all leave an item and warehouse with no live sle: cancelling the only voucher, deleting it with delete_linked_ledger_entries on, and reposting over an already emptied ledger. each asserts actual_qty, valuation_rate and stock_value are all zero.
This commit is contained in:
committed by
GitHub
parent
5fa68dd068
commit
6fbcfade6c
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user