From db3f70c0e7d8d1a23d8a3283d8e512ecc0cd11f4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 21 Jun 2026 11:37:16 +0530 Subject: [PATCH] fix(stock): case-insensitive serial-no match in get_stock_ledgers_for_serial_nos The serial-no filter used serial_batch_entry.serial_no.isin(serial_nos), which is case-sensitive on Postgres -- a differently-cased serial no missed Serial and Batch Entry rows that MariaDB (case-insensitive collation) matches (the OR'd regexp branch only covers the legacy Stock Ledger Entry.serial_no text, empty for bundle-tracked serials). Lower() both sides: MariaDB result unchanged, Postgres now matches too. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../serial_and_batch_bundle.py | 5 ++-- .../test_serial_and_batch_bundle.py | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) 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 1a2b519034f..951a696019e 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 @@ -11,7 +11,7 @@ import frappe.query_builder from frappe import _, _dict, bold from frappe.model.document import Document from frappe.model.naming import make_autoname -from frappe.query_builder.functions import Concat_ws, Max, Sum +from frappe.query_builder.functions import Concat_ws, Lower, Max, Sum from frappe.utils import ( cint, cstr, @@ -3388,7 +3388,8 @@ def get_stock_ledgers_for_serial_nos(kwargs): query.left_join(serial_batch_entry) .on(stock_ledger_entry.serial_and_batch_bundle == serial_batch_entry.parent) .where( - serial_batch_entry.serial_no.isin(serial_nos) + # Lower() both sides so serial-no matching is case-insensitive on Postgres as on MariaDB + Lower(serial_batch_entry.serial_no).isin([sn.lower() for sn in serial_nos]) | Concat_ws("", "\n", stock_ledger_entry.serial_no, "\n").regexp(regex_pattern) ) .distinct() 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 b7ac13c212d..93f071a0b1c 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 @@ -80,6 +80,31 @@ class TestSerialandBatchBundle(ERPNextTestSuite): self.assertFalse(bundle_doc.name.startswith("SABB-")) + def test_get_stock_ledgers_for_serial_nos_is_case_insensitive(self): + # get_stock_ledgers_for_serial_nos matches Serial and Batch Entry.serial_no with isin(), which is + # case-sensitive on Postgres -- a differently-cased serial no would miss entries that MariaDB + # (case-insensitive collation) matches. Lower() both sides keeps MariaDB unchanged and makes + # Postgres match too. + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_stock_ledgers_for_serial_nos, + ) + + item_code = "Test SBB Case-insensitive Serial Item" + make_item( + item_code, + {"has_serial_no": 1, "serial_no_series": "TESTCISER-.#####", "is_stock_item": 1}, + ) + pr = make_purchase_receipt(item_code=item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=100) + bundle = pr.items[0].serial_and_batch_bundle + serial_no = get_serial_nos_from_bundle(bundle)[0] + + # query with a lowercased serial no; the stored Serial and Batch Entry value is uppercase + rows = get_stock_ledgers_for_serial_nos( + frappe._dict({"item_code": item_code, "serial_nos": [serial_no.lower()]}) + ) + self.assertTrue(any(row.serial_and_batch_bundle == bundle for row in rows)) + def test_inward_outward_serial_valuation(self): from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt