From 7ecfa6b3561a697dfc536823406efc19b4c81ba8 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Mon, 31 Aug 2026 19:38:31 +0530 Subject: [PATCH] fix(stock): correct reservation and pick list quantities (#58613) --- erpnext/stock/doctype/pick_list/pick_list.py | 11 ++-- .../stock/doctype/pick_list/test_pick_list.py | 55 +++++++++++++++++++ .../stock_reservation_entry.py | 2 +- .../test_stock_reservation_entry.py | 41 ++++++++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index ab2dcba3ebc..9bbf001c385 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -1310,9 +1310,9 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) # if extra quantity is available push current warehouse to available locations if qty_diff > 0: item_location.qty = qty_diff - if item_location.serial_no: + if item_location.serial_nos: # set remaining serial numbers - item_location.serial_no = item_location.serial_no[-int(qty_diff) :] + item_location.serial_nos = item_location.serial_nos[-int(qty_diff) :] available_locations = [item_location, *available_locations] # update available locations for the item @@ -1456,13 +1456,16 @@ def filter_locations_by_picked_materials(locations, picked_item_details) -> list filterd_locations.append(row) continue if picked_qty > row.qty: - row.qty = 0 picked_item_details[key]["picked_qty"] -= row.qty + row.qty = 0 else: row.qty -= picked_qty picked_item_details[key]["picked_qty"] = 0.0 if row.serial_nos: - row.serial_nos = list(set(row.serial_nos) - set(picked_item_details[key].get("serial_no"))) + picked_serial_nos = set(picked_item_details[key].get("serial_no") or []) + row.serial_nos = [ + serial_no for serial_no in row.serial_nos if serial_no not in picked_serial_nos + ] if flt(row.qty, precision) > 0: filterd_locations.append(row) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 0cf2e4d515d..af76a4475f6 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -29,6 +29,61 @@ from erpnext.tests.utils import ERPNextTestSuite class TestPickList(ERPNextTestSuite): + def test_filter_locations_consumes_picked_qty_across_rows(self): + from erpnext.stock.doctype.pick_list.pick_list import filter_locations_by_picked_materials + + key = ("Test Warehouse", "Test Batch") + locations = [ + _dict(warehouse=key[0], batch_no=key[1], qty=5), + _dict(warehouse=key[0], batch_no=key[1], qty=5), + ] + picked_item_details = {key: {"picked_qty": 7}} + + filtered_locations = filter_locations_by_picked_materials(locations, picked_item_details) + + self.assertEqual(len(filtered_locations), 1) + self.assertEqual(filtered_locations[0].qty, 3) + self.assertEqual(picked_item_details[key]["picked_qty"], 0) + + def test_filter_locations_preserves_serial_order(self): + from erpnext.stock.doctype.pick_list.pick_list import filter_locations_by_picked_materials + + warehouse = "Test Warehouse" + locations = [ + _dict( + warehouse=warehouse, + batch_no=None, + qty=4, + serial_nos=["SN-1", "SN-2", "SN-3", "SN-4"], + ) + ] + picked_item_details = {warehouse: {"picked_qty": 2, "serial_no": ["SN-2", "SN-4"]}} + + filtered_locations = filter_locations_by_picked_materials(locations, picked_item_details) + + self.assertEqual(filtered_locations[0].serial_nos, ["SN-1", "SN-3"]) + + def test_get_items_with_location_trims_allocated_serial_nos(self): + from erpnext.stock.doctype.pick_list.pick_list import get_items_with_location_and_quantity + + item = _dict(item_code="Test Serial Item", qty=2, stock_qty=2, conversion_factor=1, uom="Nos") + item_location_map = { + item.item_code: [ + _dict( + warehouse="Test Warehouse", + batch_no=None, + qty=4, + serial_nos=["SN-1", "SN-2", "SN-3", "SN-4"], + ) + ] + } + + first_locations = get_items_with_location_and_quantity(item, item_location_map, docstatus=0) + second_locations = get_items_with_location_and_quantity(item, item_location_map, docstatus=0) + + self.assertEqual(first_locations[0].serial_no, "SN-1\nSN-2") + self.assertEqual(second_locations[0].serial_no, "SN-3\nSN-4") + def test_pick_list_allocation_takes_advisory_gate(self): if frappe.db.db_type != "postgres": return diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 30acc94ebe2..e3cbd5df111 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -1971,7 +1971,7 @@ def update_serial_batch_delivered_qty(row, name, is_cancelled=False): .where((doctype.parent == name) & (doctype.batch_no == batch_no)) ) - query.run() + query.run() def get_reserved_materials(voucher_no): diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index e10a3e6afc2..1e4145c141d 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -1168,3 +1168,44 @@ class TestStockReservationEntryValidation(ERPNextTestSuite): result = doc.get_serial_batch_entries() self.assertEqual(result.serial_nos, ["SN1", "SN2"]) self.assertEqual(result.batches["B1"], 8) + + def test_update_serial_batch_delivered_qty_updates_each_batch(self): + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + update_serial_batch_delivered_qty, + ) + + item = make_batch_item() + first_batch = frappe.get_doc(doctype="Batch", item=item.name).insert() + second_batch = frappe.get_doc(doctype="Batch", item=item.name).insert() + batches = {first_batch.name: 2, second_batch.name: 3} + sre = make_stock_reservation_entry( + item_code=item.name, + warehouse="_Test Warehouse - _TC", + reserved_qty=5, + ignore_validate=True, + do_not_submit=True, + ) + sre.reservation_based_on = "Serial and Batch" + for batch_no, qty in batches.items(): + sre.append("sb_entries", {"batch_no": batch_no, "qty": qty}) + sre.save() + + row = frappe._dict(serial_nos=[], batches=batches) + update_serial_batch_delivered_qty(row, sre.name) + delivered_qty_by_batch = { + d.batch_no: d.delivered_qty + for d in frappe.get_all( + "Serial and Batch Entry", + filters={"parent": sre.name}, + fields=["batch_no", "delivered_qty"], + ) + } + self.assertEqual(delivered_qty_by_batch, batches) + + update_serial_batch_delivered_qty(row, sre.name, is_cancelled=True) + delivered_qty = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": sre.name}, + pluck="delivered_qty", + ) + self.assertEqual(delivered_qty, [0, 0])