diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index ebaeac7049a..fcd5bc20e1e 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -2605,6 +2605,49 @@ class TestDeliveryNote(IntegrationTestCase): self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.per_returned, 100) + def test_packed_item_serial_no_status(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + from erpnext.stock.doctype.item.test_item import make_item + + # test Update Items with product bundle + if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"): + bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item( + "_Packed Item New Sn Item", + {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PACKED-NEW-.#####"}, + ) + make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New Sn Item"], 1) + + make_stock_entry(item="_Packed Item New Sn Item", target="_Test Warehouse - _TC", qty=5, rate=100) + + dn = create_delivery_note( + item_code="_Test Product Bundle Item New 1", + warehouse="_Test Warehouse - _TC", + qty=5, + ) + + dn.reload() + + serial_nos = [] + for row in dn.packed_items: + self.assertTrue(row.serial_and_batch_bundle) + doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle) + for row in doc.entries: + status = frappe.db.get_value("Serial No", row.serial_no, "status") + self.assertEqual(status, "Delivered") + serial_nos.append(row.serial_no) + + dn.cancel() + + for row in serial_nos: + status = frappe.db.get_value("Serial No", row, "status") + self.assertEqual(status, "Active") + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index a44a9e5deef..c7ef426d659 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -34,6 +34,7 @@ "returned_against", "section_break_wzou", "is_cancelled", + "is_packed", "is_rejected", "amended_from" ], @@ -251,12 +252,19 @@ "label": "Naming Series", "options": "\nSABB-.########", "set_only_once": 1 + }, + { + "default": "0", + "fieldname": "is_packed", + "fieldtype": "Check", + "label": "Is Packed" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-02-17 16:22:36.056205", + "modified": "2025-05-30 18:05:55.489195", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", @@ -390,8 +398,9 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "item_code" -} \ No newline at end of file +} 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 44f7dd74cd4..b848b9fddba 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 @@ -65,6 +65,7 @@ class SerialandBatchBundle(Document): has_batch_no: DF.Check has_serial_no: DF.Check is_cancelled: DF.Check + is_packed: DF.Check is_rejected: DF.Check item_code: DF.Link item_group: DF.Link | None diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index bff764228f5..f06706ee21e 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -111,6 +111,7 @@ class SerialBatchBundle: "type_of_transaction": "Inward" if self.sle.actual_qty > 0 else "Outward", "company": self.company, "is_rejected": self.is_rejected_entry(), + "is_packed": self.is_packed_entry(), "make_bundle_from_sle": 1, } ).make_serial_and_batch_bundle() @@ -194,7 +195,21 @@ class SerialBatchBundle: elif sn_doc.has_batch_no and len(sn_doc.entries) == 1: values_to_update["batch_no"] = sn_doc.entries[0].batch_no - frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update) + doctype = self.child_doctype + name = self.sle.voucher_detail_no + if sn_doc.is_packed: + doctype = "Packed Item" + name = frappe.db.get_value( + "Packed Item", + { + "parent_detail_docname": sn_doc.voucher_detail_no, + "item_code": self.sle.item_code, + "serial_and_batch_bundle": ("is", "not set"), + }, + "name", + ) + + frappe.db.set_value(doctype, name, values_to_update) @property def child_doctype(self): @@ -217,6 +232,19 @@ class SerialBatchBundle: def is_rejected_entry(self): return is_rejected(self.sle.voucher_type, self.sle.voucher_detail_no, self.sle.warehouse) + def is_packed_entry(self): + if self.sle.voucher_type in ["Delivery Note", "Sales Invoice"]: + item_code = frappe.db.get_value( + self.sle.voucher_type + " Item", + self.sle.voucher_detail_no, + "item_code", + ) + + if item_code != self.sle.item_code: + return frappe.db.get_value("Item", item_code, "is_stock_item") == 0 + + return False + def process_batch_no(self): if ( not self.sle.is_cancelled @@ -268,6 +296,10 @@ class SerialBatchBundle: update_values["rejected_serial_and_batch_bundle"] = "" frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, update_values) + if self.child_doctype == "Delivery Note": + frappe.db.set_value( + "Packed Item", {"parent_detail_docname": self.sle.voucher_detail_no}, update_values + ) frappe.db.set_value( "Serial and Batch Bundle",