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) <noreply@anthropic.com>
This commit is contained in:
Mihir Kandoi
2026-06-21 11:37:16 +05:30
parent 9389ce6d9a
commit db3f70c0e7
2 changed files with 28 additions and 2 deletions

View File

@@ -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()

View File

@@ -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