mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-19 11:27:55 +00:00
fix(stock): reserve product bundle components from a Pick List (#59134)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user