From fb132225d7dcbb737ff81ee2164ccc119da686d8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:14:16 +0000 Subject: [PATCH] fix: user not able to set valuation rate zero in stock reco (backport #58800) (#58823) fix: user not able to set valuation rate zero in stock reco (#58800) * fix: user not able to set valuation rate zero in stock reco * fix: wrong difference amount when valuation rate is zero * fix: blank valuation rate should not be treated as a change (cherry picked from commit e85e300f8f90962c30710ff38a54b7da4abc7cdc) Co-authored-by: rohitwaghchaure --- .../stock_reconciliation.py | 10 ++- .../test_stock_reconciliation.py | 80 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 12cda4cb691..c89c9e0138b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -532,7 +532,10 @@ class StockReconciliation(StockController): rate_precision = item.precision("valuation_rate") rate = flt(item_dict.get("rate"), rate_precision) - valuation_rate = flt(item.valuation_rate, rate_precision) if item.valuation_rate else None + # an unset rate means "keep the current one", an explicit zero is a real revaluation + valuation_rate = ( + flt(item.valuation_rate, rate_precision) if item.valuation_rate not in ("", None) else None + ) if ( (item.qty is None or item.qty == item_dict.get("qty")) and (valuation_rate is None or valuation_rate == rate) @@ -575,7 +578,10 @@ class StockReconciliation(StockController): amount_precision = item.precision("amount") new_qty = flt(item.qty, qty_precision) - new_valuation_rate = flt(item.valuation_rate or item_dict.get("rate")) + # an explicitly set zero rate is a real revaluation, don't fall back to the current rate + new_valuation_rate = flt( + item.valuation_rate if item.valuation_rate not in ("", None) else item_dict.get("rate") + ) current_qty = flt(item_dict.get("qty"), qty_precision) current_valuation_rate = flt(item_dict.get("rate")) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 37e2eb840c6..ec4fdc0cd35 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -27,6 +27,7 @@ from erpnext.stock.tests.test_utils import StockTestMixin from erpnext.stock.utils import ( get_combine_datetime, get_incoming_rate, + get_stock_balance, get_stock_value_on, get_valuation_method, ) @@ -1588,6 +1589,85 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin): self.assertEqual(sr.difference_amount, 100 * -1) self.assertTrue(sr.items[0].qty == 0) + def test_difference_amount_for_zero_valuation_rate(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item("Test Item Stock Reco Zero Valuation Rate").name + warehouse = "_Test Warehouse - _TC" + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=5, rate=0, do_not_save=1 + ) + sr.items[0].allow_zero_valuation_rate = 1 + sr.save() + + # qty is unchanged, the stock is revalued from 5 x 100 to 5 x 0 + self.assertEqual(sr.items[0].current_valuation_rate, 100) + self.assertEqual(sr.items[0].valuation_rate, 0) + self.assertEqual(sr.difference_amount, -500) + + sr.submit() + sr.reload() + + self.assertEqual(sr.difference_amount, -500) + self.assertEqual( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": sr.name, "is_cancelled": 0}, + "stock_value_difference", + ), + -500, + ) + + def test_no_change_row_removed_when_valuation_rate_is_blank(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item("Test Item Stock Reco Blank Valuation Rate").name + warehouse = "_Test Warehouse - _TC" + + make_stock_entry(item_code=item_code, target=warehouse, qty=5, basic_rate=100) + + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=5, rate=None, do_not_save=1 + ) + + # a blank rate means "keep the current rate", so nothing changed on this row + self.assertRaises(EmptyStockReconciliationItemsError, sr.save) + + def test_set_existing_stock_valuation_to_zero(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item("Test Item Stock Reco Set Valuation Zero").name + warehouse = "_Test Warehouse - _TC" + + make_stock_entry(item_code=item_code, target=warehouse, qty=10, basic_rate=50) + + sr = create_stock_reconciliation( + item_code=item_code, warehouse=warehouse, qty=10, rate=0, do_not_save=1 + ) + sr.items[0].allow_zero_valuation_rate = 1 + + # only the rate changes, the row must not be dropped as "no change" + sr.save() + self.assertEqual(len(sr.items), 1) + + sr.submit() + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": sr.name, "is_cancelled": 0}, + ["qty_after_transaction", "valuation_rate", "stock_value"], + as_dict=True, + ) + + self.assertEqual(sle.qty_after_transaction, 10) + self.assertEqual(sle.valuation_rate, 0) + self.assertEqual(sle.stock_value, 0) + + self.assertEqual(get_stock_balance(item_code, warehouse, with_valuation_rate=True), (10, 0.0)) + def test_stock_reco_recalculate_qty_for_backdated_entry(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry