mirror of
https://github.com/frappe/erpnext.git
synced 2026-10-29 22:57:42 +00:00
fix(stock): return packed items with serial / batch fields
A return of a product bundle failed with "No Serial / Batches are available for return" when the original packed items had serial or batch numbers in the fields, but no Serial and Batch Bundle. Delivery notes from before v15 have this shape. The return looked only for bundles of the original packed items. Now it skips packed rows that have serial or batch fields, and the bundle is made from these fields. Also, a new document matched its stale packed rows by the idx of the packed row and the idx of the item row. Only the first component of a bundle matched, so the other components lost their copied serial and batch fields. Now the stale rows are used in sequence per parent item and component.
This commit is contained in:
@@ -524,7 +524,13 @@ class StockController(AccountsController):
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
if table_name == "packed_items" and row.get("parent_detail_docname"):
|
||||
# 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)
|
||||
):
|
||||
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):
|
||||
|
||||
@@ -3316,6 +3316,50 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
|
||||
)
|
||||
|
||||
def test_sales_return_for_product_bundle_with_legacy_serial_nos(self):
|
||||
bundle_item = make_item("_Packed Legacy Bundle Item", {"is_stock_item": 0}).name
|
||||
stock_item = make_item("_Packed Legacy Stock Item", {"is_stock_item": 1}).name
|
||||
serial_items = [
|
||||
make_item(
|
||||
f"_Packed Legacy Serial Item {i}",
|
||||
{"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": f"SN-PACKED-LEGACY-{i}-.#####"},
|
||||
).name
|
||||
for i in (1, 2)
|
||||
]
|
||||
for item_code in [stock_item, *serial_items]:
|
||||
make_stock_entry(item=item_code, target="_Test Warehouse - _TC", qty=2, rate=100)
|
||||
make_product_bundle(bundle_item, [stock_item, *serial_items])
|
||||
|
||||
dn = create_delivery_note(item_code=bundle_item, warehouse="_Test Warehouse - _TC", qty=2)
|
||||
dn.reload()
|
||||
|
||||
# Delivery notes from before v15 have serial nos in the packed items without a bundle
|
||||
delivered_serial_nos = {}
|
||||
for packed_item in dn.packed_items:
|
||||
if packed_item.item_code not in serial_items:
|
||||
continue
|
||||
|
||||
serial_nos = get_serial_nos_from_bundle(packed_item.serial_and_batch_bundle)
|
||||
delivered_serial_nos[packed_item.item_code] = sorted(serial_nos)
|
||||
legacy_values = {"serial_no": "\n".join(serial_nos), "serial_and_batch_bundle": None}
|
||||
packed_item.db_set(legacy_values)
|
||||
frappe.db.set_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": dn.name, "item_code": packed_item.item_code},
|
||||
legacy_values,
|
||||
)
|
||||
|
||||
return_dn = make_sales_return(dn.name)
|
||||
return_dn.submit()
|
||||
return_dn.reload()
|
||||
|
||||
returned_serial_nos = {
|
||||
row.item_code: sorted(get_serial_nos_from_bundle(row.serial_and_batch_bundle))
|
||||
for row in return_dn.packed_items
|
||||
if row.item_code in serial_items
|
||||
}
|
||||
self.assertEqual(returned_serial_nos, 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 (
|
||||
|
||||
@@ -107,13 +107,16 @@ def get_indexed_packed_items_table(doc):
|
||||
Use: to quickly retrieve/check if row existed in table instead of looping n times.
|
||||
A component repeated on a bundle shares one key, so rows are kept as a list and
|
||||
consumed per occurrence rather than collapsed onto a single row.
|
||||
|
||||
A new doc (e.g. mapped) has no link to its item rows yet, so its packed rows are
|
||||
consumed in order per parent item and component.
|
||||
"""
|
||||
indexed_table = {}
|
||||
for packed_item in doc.get("packed_items"):
|
||||
key = (
|
||||
packed_item.parent_item,
|
||||
packed_item.item_code,
|
||||
packed_item.idx if doc.is_new() else packed_item.parent_detail_docname,
|
||||
None if doc.is_new() else packed_item.parent_detail_docname,
|
||||
)
|
||||
|
||||
indexed_table.setdefault(key, []).append(packed_item)
|
||||
@@ -179,7 +182,7 @@ def add_packed_item_row(doc, packing_item, main_item_row, packed_items_table, re
|
||||
key = (
|
||||
main_item_row.item_code,
|
||||
packing_item.item_code,
|
||||
main_item_row.idx if doc.is_new() else main_item_row.name,
|
||||
None if doc.is_new() else main_item_row.name,
|
||||
)
|
||||
# a component repeated on the bundle shares one key; consume stale rows in order so
|
||||
# each occurrence keeps its own packed row instead of overwriting a single one
|
||||
|
||||
Reference in New Issue
Block a user