mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-26 21:35:19 +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.
(cherry picked from commit 6fbcfade6c)
This commit is contained in:
committed by
Mergify
parent
f71bc4d3ef
commit
37a1fd11e9
@@ -55,6 +55,68 @@ class TestBin(ERPNextTestSuite):
|
|||||||
self.assertEqual(bin.valuation_rate, 0)
|
self.assertEqual(bin.valuation_rate, 0)
|
||||||
self.assertEqual(bin.stock_value, 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):
|
def test_index_exists(self):
|
||||||
indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
|
indexes = frappe.db.sql("show index from tabBin where Non_unique = 0", as_dict=1)
|
||||||
if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
|
if not any(index.get("Key_name") == "unique_item_warehouse" for index in indexes):
|
||||||
|
|||||||
@@ -587,16 +587,6 @@ class update_entries_after:
|
|||||||
previous_sle = get_previous_sle_of_current_voucher(args)
|
previous_sle = get_previous_sle_of_current_voucher(args)
|
||||||
if previous_sle:
|
if previous_sle:
|
||||||
self.prev_sle_dict[(args.get("item_code"), args.get("warehouse"))] = 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
|
warehouse_dict.previous_sle = previous_sle
|
||||||
|
|
||||||
@@ -1800,6 +1790,30 @@ class update_entries_after:
|
|||||||
|
|
||||||
frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
|
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):
|
def get_sle_against_current_voucher(kwargs):
|
||||||
kwargs["posting_datetime"] = get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
|
kwargs["posting_datetime"] = get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
|
||||||
|
|||||||
Reference in New Issue
Block a user