fix(stock): seed bin values when cancelling a stock voucher (#59250)

* fix(stock): seed bin values when cancelling a stock voucher

cancellation flags every sle of the voucher before update_entries_after
runs, so get_sle_against_current_voucher returns nothing and the seeding
added in #57380 never fires. prev_sle_dict stays empty, update_bin()
writes nothing, and the bin keeps the stock value and valuation rate it
had before the cancellation while its quantity is restored.

seed from the args when the query comes back empty, leaving the existing
anchor in place whenever a live entry shares the posting datetime.

* test(stock): cover bin stock value after cancelling a transfer

a transfer between two warehouses that both hold stock, then cancelled:
both bins must return to their previous quantity, valuation rate and
stock value. fails on develop with 500.0 != 1000.

(cherry picked from commit 62cb38d36b)
This commit is contained in:
Sudharsanan Ashok
2026-09-22 17:53:07 +05:30
committed by Mergify
parent 6af589208b
commit 3ab13e12d4
2 changed files with 26 additions and 3 deletions

View File

@@ -96,6 +96,27 @@ class TestBin(ERPNextTestSuite):
self.assertEqual(bin.valuation_rate, 0)
self.assertEqual(bin.stock_value, 0)
def test_cancelling_transfer_restores_bin_stock_value(self):
"""Cancelling a transfer must put back the stock value of both bins, not just the quantity."""
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item_code = make_item().name
source = "_Test Warehouse - _TC"
target = "_Test Warehouse 1 - _TC"
make_stock_entry(item_code=item_code, target=source, qty=10, rate=100, posting_time="01:00:00")
make_stock_entry(item_code=item_code, target=target, qty=10, rate=200, posting_time="02:00:00")
se = make_stock_entry(
item_code=item_code, source=source, target=target, qty=5, posting_time="03:00:00"
)
se.cancel()
for warehouse, valuation_rate, stock_value in ((source, 100, 1000), (target, 200, 2000)):
bin = frappe.get_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
self.assertEqual(bin.actual_qty, 10)
self.assertEqual(bin.valuation_rate, valuation_rate)
self.assertEqual(bin.stock_value, stock_value)
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

View File

@@ -965,8 +965,10 @@ class update_entries_after:
def process_sle_against_current_timestamp(self):
sl_entries = get_sle_against_current_voucher(self.args)
if self.args.get("cancelled") and sl_entries:
self.seed_previous_sle_for_cancellation(sl_entries[0])
if self.args.get("cancelled"):
# Cancellation flags every entry of the voucher first, so this query usually returns
# nothing and the args are the only anchor left to seed the previous values from.
self.seed_previous_sle_for_cancellation(sl_entries[0] if sl_entries else self.args)
for sle in sl_entries:
sle["timestamp"] = sle.posting_datetime
self.process_sle(sle)
@@ -977,7 +979,7 @@ class update_entries_after:
return
args = frappe._dict(anchor_sle)
args["sle_id"] = args.name
args["sle_id"] = args.get("name")
prev_sle = get_previous_sle_of_current_voucher(args)
if prev_sle:
self.prev_sle_dict[key] = prev_sle