From 5aaca83fe460500c73de4d3194891898580d72d3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:11:20 +0000 Subject: [PATCH] fix: remove reference in serial/batch when document is cancelled (backport #53979) (#53989) --- .../serial_and_batch_bundle.py | 38 +++++++++++++++++++ .../test_serial_and_batch_bundle.py | 32 ++++++++++++++++ 2 files changed, 70 insertions(+) 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 45790ed89c4..5220c2b6274 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 @@ -1489,6 +1489,7 @@ class SerialandBatchBundle(Document): def on_cancel(self): self.validate_voucher_no_docstatus() self.validate_batch_quantity() + self.remove_source_document_no() def validate_batch_quantity(self): if not self.has_batch_no: @@ -1507,6 +1508,43 @@ class SerialandBatchBundle(Document): if flt(available_qty, precision) < 0: self.throw_negative_batch(d.batch_no, available_qty, precision) + def remove_source_document_no(self): + if not self.has_serial_no and not self.has_batch_no: + return + + if self.total_qty <= 0: + return + + if self.has_serial_no: + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + sn_table = frappe.qb.DocType("Serial No") + ( + frappe.qb.update(sn_table) + .set(sn_table.reference_doctype, None) + .set(sn_table.reference_name, None) + .set(sn_table.posting_date, None) + .where( + (sn_table.name.isin(serial_nos)) + & (sn_table.reference_doctype == self.voucher_type) + & (sn_table.reference_name == self.voucher_no) + & (sn_table.posting_date == getdate(self.posting_datetime)) + ) + ).run() + + if self.has_batch_no: + batch_nos = [d.batch_no for d in self.entries if d.batch_no] + batch_table = frappe.qb.DocType("Batch") + ( + frappe.qb.update(batch_table) + .set(batch_table.reference_doctype, None) + .set(batch_table.reference_name, None) + .where( + (batch_table.name.isin(batch_nos)) + & (batch_table.reference_doctype == self.voucher_type) + & (batch_table.reference_name == self.voucher_no) + ) + ).run() + def throw_negative_batch(self, batch_no, available_qty, precision, posting_datetime=None): from erpnext.stock.stock_ledger import NegativeStockError diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index c6929fe4cdb..ab360d8133b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -1077,6 +1077,38 @@ class TestSerialandBatchBundle(ERPNextTestSuite): self.assertTrue(bundle_doc.docstatus == 0) self.assertRaises(frappe.ValidationError, bundle_doc.submit) + def test_reference_voucher_on_cancel(self): + """ + When a source document is cancelled, the reference voucher field + in the respective serial or batch document should be nullified. + """ + + item_code = make_item( + "Serial Item", + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SERIAL.#####", + }, + ).name + + se = make_stock_entry( + item_code=item_code, + qty=1, + target="_Test Warehouse - _TC", + ) + serial_no = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle)[0] + self.assertEqual(frappe.get_value("Serial No", serial_no, "reference_name"), se.name) + + se.cancel() + self.assertIsNone(frappe.get_value("Serial No", serial_no, "reference_name")) + + se1 = frappe.copy_doc(se, ignore_no_copy=False) + se1.items[0].serial_no = serial_no + se1.submit() + + self.assertEqual(frappe.get_value("Serial No", serial_no, "reference_name"), se1.name) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos