From ac46b3d1cab3a0bdf08c7e772bbbc28b7c859d73 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 9 Oct 2025 01:00:31 +0530 Subject: [PATCH] fix: sales return for product bundle items (cherry picked from commit 13ce7279a87a538fae272748b0e84e4efb673b7c) --- .../controllers/sales_and_purchase_return.py | 22 +++++++++++++--- erpnext/controllers/selling_controller.py | 9 ++++++- erpnext/controllers/stock_controller.py | 26 ++++++++++++++++++- .../serial_and_batch_bundle.py | 14 ++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index a45b5813584..f45b06b01b0 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -852,10 +852,10 @@ def get_available_serial_batches(field, doctype, reference_ids, is_rejected=Fals if not _bundle_ids: return frappe._dict({}) - return get_serial_batches_based_on_bundle(field, _bundle_ids) + return get_serial_batches_based_on_bundle(doctype, field, _bundle_ids) -def get_serial_batches_based_on_bundle(field, _bundle_ids): +def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids): available_dict = frappe._dict({}) batch_serial_nos = frappe.get_all( "Serial and Batch Bundle", @@ -867,6 +867,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): "`tabSerial and Batch Bundle`.`voucher_detail_no`", "`tabSerial and Batch Bundle`.`voucher_type`", "`tabSerial and Batch Bundle`.`voucher_no`", + "`tabSerial and Batch Bundle`.`item_code`", ], filters=[ ["Serial and Batch Bundle", "name", "in", _bundle_ids], @@ -880,6 +881,9 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"): key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field) + if doctype == "Packed Item": + key = (row.item_code, key) + if row.voucher_type in ["Sales Invoice", "Delivery Note"]: row.qty = -1 * row.qty @@ -908,6 +912,8 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids): def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False): filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} + if doctype == "Packed Item": + filters = {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")} pluck_field = "serial_and_batch_bundle" if is_rejected: @@ -921,10 +927,14 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False pluck=pluck_field, ) + if _bundle_ids and doctype == "Packed Item": + return _bundle_ids + if not _bundle_ids: return {} - del filters["name"] + if "name" in filters: + del filters["name"] filters[field] = ("in", reference_ids) @@ -971,6 +981,9 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field if not qty_field: qty_field = "stock_qty" + if not hasattr(row, qty_field): + qty_field = "qty" + if not warehouse_field: warehouse_field = "warehouse" @@ -1060,6 +1073,9 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f if not qty_field: qty_field = "stock_qty" + if not hasattr(child_doc, qty_field): + qty_field = "qty" + warehouse = child_doc.get(warehouse_field) if parent_doc.get("is_internal_customer"): warehouse = child_doc.get("target_warehouse") diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d85c1b28f97..8fc49d09c0e 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -517,8 +517,15 @@ class SellingController(StockController): if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): continue + item_details = frappe.get_cached_value( + "Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) + if not self.get("return_against") or ( - get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + get_valuation_method(d.item_code) == "Moving Average" + and self.get("is_return") + and not item_details.has_serial_no + and not item_details.has_batch_no ): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty")) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 3b5dbf2ee81..d7c9bb61a20 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -335,10 +335,20 @@ class StockController(AccountsController): return child_doctype = self.doctype + " Item" + if table_name == "packed_items": + field = "parent_detail_docname" + child_doctype = "Packed Item" + available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids) for row in self.get(table_name): - if data := available_dict.get(row.get(field)): + value = row.get(field) + if table_name == "packed_items" and row.get("parent_detail_docname"): + value = self.get_value_for_packed_item(row) + if not value: + continue + + if data := available_dict.get(value): data = filter_serial_batches(self, data, row) bundle = make_serial_batch_bundle_for_return(data, row, self) row.db_set( @@ -354,6 +364,14 @@ class StockController(AccountsController): "incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") ) + def get_value_for_packed_item(self, row): + parent_items = self.get("items", {"name": row.parent_detail_docname}) + if parent_items: + ref = parent_items[0].get("dn_detail") + return (row.item_code, ref) + + return None + def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]: field = { "Sales Invoice": "sales_invoice_item", @@ -388,6 +406,12 @@ class StockController(AccountsController): ): reference_ids.append(row.get(field)) + 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): + reference_ids.append(d.get(field)) + return field, reference_ids @frappe.request_cache diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 9c69d856d45..d8d68f34fbb 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -638,6 +638,17 @@ class SerialandBatchBundle(Document): if not rate and self.voucher_detail_no and self.voucher_no: rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) + is_packed_item = False + if rate is None and child_table == "Delivery Note Item": + rate = frappe.db.get_value( + "Packed Item", + self.voucher_detail_no, + "incoming_rate", + ) + + if rate is not None: + is_packed_item = True + stock_queue = [] batches = [] if prev_sle and prev_sle.stock_queue: @@ -659,6 +670,9 @@ class SerialandBatchBundle(Document): elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference: continue + if is_packed_item and d.incoming_rate: + rate = d.incoming_rate + d.incoming_rate = flt(rate) if d.qty: d.stock_value_difference = flt(d.qty) * d.incoming_rate