fix(stock): return packed items against bundle if source has one

With the setting "Use Serial / Batch Fields", a packed item keeps its
serial nos and also gets a bundle. A return of this packed item skipped
the bundle and made a new bundle from all copied serial nos. A partial
return then failed, because the bundle qty was more than the return qty.

Now the return skips the bundle only if the original packed item has no
bundle (e.g. delivered before v15).
This commit is contained in:
barredterra
2026-10-28 17:02:36 +01:00
parent f8c45bd9ed
commit 6688c3ca96
2 changed files with 51 additions and 8 deletions
+21 -8
View File
@@ -524,20 +524,33 @@ class StockController(AccountsController):
):
reference_ids.append(row.get(field))
# Packed items with serial / batch fields (e.g. delivered before v15) can have no bundle
# to return against, make_bundle_using_old_serial_batch_fields handles them
if (
table_name == "packed_items"
and row.get("parent_detail_docname")
and not (row.serial_no or row.batch_no)
):
if table_name == "packed_items" and row.get("parent_detail_docname"):
parent_rows = self.get("items", {"name": row.parent_detail_docname}) or []
for d in parent_rows:
if d.get(field) and not d.get(bundle_field):
if (
d.get(field)
and not d.get(bundle_field)
and not self.is_legacy_packed_item(row, d.get(field))
):
reference_ids.append(d.get(field))
return field, reference_ids
def is_legacy_packed_item(self, row, source_parent_detail_docname):
"""Packed items delivered before v15 have serial / batch fields, but no bundle to return
against. make_bundle_using_old_serial_batch_fields handles them."""
if not (row.serial_no or row.batch_no):
return False
return not frappe.db.exists(
"Packed Item",
{
"parent_detail_docname": source_parent_detail_docname,
"item_code": row.item_code,
"serial_and_batch_bundle": ("is", "set"),
},
)
@frappe.request_cache
def is_serial_batch_item(self, item_code) -> bool:
if not frappe.db.exists("Item", item_code):
@@ -3360,6 +3360,36 @@ class TestDeliveryNote(FrappeTestCase):
}
self.assertEqual(returned_serial_nos, delivered_serial_nos)
@change_settings("Stock Settings", {"use_serial_batch_fields": 1})
def test_partial_sales_return_for_product_bundle_with_serial_fields_and_bundle(self):
bundle_item = make_item("_Packed Serial Fields Bundle Item", {"is_stock_item": 0}).name
serial_item = make_item(
"_Packed Serial Fields Serial Item",
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PACKED-FIELDS-.#####"},
).name
se = make_stock_entry(item=serial_item, target="_Test Warehouse - _TC", qty=2, rate=100)
delivered_serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)
make_product_bundle(bundle_item, [serial_item])
# With serial / batch fields enabled, the packed item keeps its serial nos and gets a bundle
dn = create_delivery_note(
item_code=bundle_item, warehouse="_Test Warehouse - _TC", qty=2, do_not_submit=True
)
dn.packed_items[0].serial_no = "\n".join(delivered_serial_nos)
dn.submit()
dn.reload()
self.assertTrue(dn.packed_items[0].serial_no)
self.assertTrue(dn.packed_items[0].serial_and_batch_bundle)
return_dn = make_sales_return(dn.name)
return_dn.items[0].qty = -1
return_dn.submit()
return_dn.reload()
returned_serial_nos = get_serial_nos_from_bundle(return_dn.packed_items[0].serial_and_batch_bundle)
self.assertEqual(len(returned_serial_nos), 1)
self.assertIn(returned_serial_nos[0], delivered_serial_nos)
@change_settings("Stock Settings", {"allow_negative_stock": 0, "enable_stock_reservation": 1})
def test_partial_delivery_note_against_reserved_stock(self):
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (