fix(stock): reserve product bundle components from a Pick List (#59134)

This commit is contained in:
Mihir Kandoi
2026-09-17 13:58:02 +05:30
committed by GitHub
parent 0f66c41819
commit 00b7e6e9eb
3 changed files with 73 additions and 13 deletions

View File

@@ -85,7 +85,7 @@ class SalesOrderStockReservation:
create_stock_reservation_entries_for_so_items as create_stock_reservation_entries,
)
packed_items = self._extract_packed_item_details(items_details)
packed_items = self._extract_packed_item_details(items_details, from_voucher_type)
sre_count = 0
if items_details != []:
@@ -100,17 +100,28 @@ class SalesOrderStockReservation:
if items:
self._reserve_packed_items(items, sre_count, notify)
def _extract_packed_item_details(self, items_details: list[dict] | None) -> list:
"""Pull packed-item rows (whose Sales Order Item no longer exists) out of items_details."""
packed_items = []
if items_details:
for item in items_details:
if not frappe.db.exists("Sales Order Item", item.get("sales_order_item")):
item["qty"] = item.pop("qty_to_reserve")
packed_items.append(item)
def _extract_packed_item_details(
self, items_details: list[dict] | None, from_voucher_type: str | None = None
) -> list:
"""Pull packed-item rows (whose Sales Order Item no longer exists) out of items_details
and rewrite them into the payload StockReservation reads."""
if not items_details:
return []
for item in packed_items:
items_details.remove(item)
packed_items = [
item
for item in items_details
if not frappe.db.exists("Sales Order Item", item.get("sales_order_item"))
]
for item in packed_items:
items_details.remove(item)
item["qty"] = item.pop("qty_to_reserve")
item["from_voucher_type"] = from_voucher_type
picked_bundle = item.pop("serial_and_batch_bundle", None)
if picked_bundle:
item["serial_and_batch_bundles"] = [picked_bundle]
return packed_items

View File

@@ -546,14 +546,18 @@ class PickList(TransactionBase):
@frappe.whitelist(methods=["POST"])
def create_stock_reservation_entries(self, notify: bool = True) -> None:
"""Creates Stock Reservation Entries for Sales Order Items against Pick List."""
"""Creates Stock Reservation Entries for Sales Order Items against Pick List.
A bundle component reserves against its Packed Item, because the bundle itself
is a non-stock Sales Order Item and can never hold reserved stock.
"""
self.check_permission("write")
so_items_details_map = {}
for location in self.locations:
if location.warehouse and location.sales_order and location.sales_order_item:
item_details = {
"sales_order_item": location.sales_order_item,
"sales_order_item": location.product_bundle_item or location.sales_order_item,
"item_code": location.item_code,
"warehouse": location.warehouse,
"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),

View File

@@ -852,6 +852,51 @@ class TestStockReservationEntry(ERPNextTestSuite):
# Test - 3: Reserved Serial/Batch Nos should be equal to Picked Serial/Batch Nos.
self.assertSetEqual(picked_sb_details, reserved_sb_details)
@ERPNextTestSuite.change_settings(
"Stock Settings",
{
"allow_negative_stock": 0,
"enable_stock_reservation": 1,
"allow_partial_reservation": 1,
},
)
def test_stock_reservation_from_pick_list_for_product_bundle(self) -> None:
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
bundle, components = create_product_bundle(quantities=[2, 3], warehouse=self.warehouse)
so = make_sales_order(item_code=bundle, qty=2, warehouse=self.warehouse)
pl = create_pick_list(so.name)
pl.save()
pl.submit()
pl.create_stock_reservation_entries()
pl.reload()
so.reload()
packed_item_by_code = {row.item_code: row for row in so.packed_items}
self.assertEqual(len(pl.locations), len(components))
for location in pl.locations:
packed_item = packed_item_by_code[location.item_code]
# Test - 1: Bundle components should be reserved against their Packed Item.
sre_details = _get_stock_reservation_entries_for_voucher(
"Sales Order", so.name, packed_item.name, fields=["reserved_qty", "from_voucher_type"]
)
self.assertEqual(len(sre_details), 1)
self.assertEqual(sre_details[0].reserved_qty, packed_item.qty)
self.assertEqual(sre_details[0].from_voucher_type, "Pick List")
# Test - 2: Reserved Qty should be updated in Pick List Item.
self.assertEqual(location.stock_reserved_qty, location.picked_qty)
pl.cancel_stock_reservation_entries()
pl.reload()
# Test - 3: Unreserving from the Pick List should clear the reserved qty.
for location in pl.locations:
self.assertEqual(location.stock_reserved_qty, 0)
@ERPNextTestSuite.change_settings(
"Stock Settings",
{