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 c8558e17474..9cf0ef8e622 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 @@ -311,6 +311,30 @@ class SerialandBatchBundle(Document): SerialNoDuplicateError, ) + if ( + self.voucher_type == "Stock Entry" + and self.type_of_transaction == "Inward" + and frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose") + in ["Manufacture", "Repack"] + ): + serial_nos = frappe.get_all( + "Serial No", filters={"name": ("in", serial_nos), "status": "Delivered"}, pluck="name" + ) + + if serial_nos: + if len(serial_nos) == 1: + frappe.throw( + _( + "Serial No {0} is already Delivered. You cannot use them again in Manufacture / Repack entry." + ).format(bold(serial_nos[0])) + ) + else: + frappe.throw( + _( + "Serial Nos {0} are already Delivered. You cannot use them again in Manufacture / Repack entry." + ).format(bold(", ".join(serial_nos))) + ) + def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index d0143941660..98205a7d8f3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2147,6 +2147,45 @@ class TestStockEntry(IntegrationTestCase): self.assertEqual(incoming_rate, 125.0) + def test_prevent_reuse_delivered_serial_no_in_repack(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item = "Test Prevent Reuse Delivered Serial No" + warehouse = "_Test Warehouse - _TC" + + item_doc = make_item(item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SHGJ.####"}) + + make_stock_entry(item_code="_Test Item", target=warehouse, qty=2, rate=100) + make_stock_entry(item_code=item, target=warehouse, qty=2, rate=100) + + dn = create_delivery_note(item_code=item, qty=2) + delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0] + + se = make_stock_entry( + item_code="_Test Item", source=warehouse, qty=1, purpose="Repack", do_not_save=True + ) + se.append( + "items", + { + "item_code": item_doc.name, + "item_name": item_doc.item_name, + "s_warehouse": None, + "t_warehouse": warehouse, + "description": item_doc.description, + "uom": item_doc.stock_uom, + "qty": 1, + "use_serial_batch_fields": 1, + "serial_no": delivered_serial_no, + }, + ) + + se.save() + status = frappe.db.get_value("Serial No", delivered_serial_no, "status") + + self.assertEqual(status, "Delivered") + self.assertEqual(se.purpose, "Repack") + self.assertRaises(frappe.ValidationError, se.submit) + def make_serialized_item(self, **args): args = frappe._dict(args) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index ca40f00df6e..732faefe38f 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -419,12 +419,7 @@ class SerialBatchBundle: self.update_serial_no_status_warehouse(self.sle, serial_nos) - def update_serial_no_status_warehouse(self, sle, serial_nos): - warehouse = sle.warehouse if sle.actual_qty > 0 else None - - if isinstance(serial_nos, str): - serial_nos = [serial_nos] - + def get_status_for_serial_nos(self, sle): status = "Inactive" if sle.actual_qty < 0: status = "Delivered" @@ -438,6 +433,23 @@ class SerialBatchBundle: ]: status = "Consumed" + if sle.is_cancelled == 1 and ( + sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed" + ): + status = "Inactive" + + return status + + def update_serial_no_status_warehouse(self, sle, serial_nos): + warehouse = sle.warehouse if sle.actual_qty > 0 else None + + if isinstance(serial_nos, str): + serial_nos = [serial_nos] + + status = "Active" + if not warehouse: + status = self.get_status_for_serial_nos(sle) + customer = None if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0: customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")