fix(website): case-insensitive Item Variant attribute match on Postgres

get_item_codes_by_attributes compared Item Variant Attribute `attribute`/`attribute_value`
with raw equality/IN, which is case-sensitive on Postgres -- a differently-cased website
filter value missed variants that MariaDB (case-insensitive collation) matches. Lower()
both sides: MariaDB result is unchanged (already case-insensitive), 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:30:29 +05:30
parent 3d9b704730
commit 9389ce6d9a
2 changed files with 31 additions and 2 deletions

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.query_builder.functions import Lower
from frappe.utils import cint, cstr, flt, fmt_money
from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item
@@ -133,9 +134,12 @@ def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
frappe.qb.from_(iva)
.select(iva.parent)
# attribute_value is a varchar column; cast values to str so postgres doesn't choke on
# `varchar = numeric` for numeric attributes (stored values are strings on both backends)
# `varchar = numeric` for numeric attributes (stored values are strings on both backends).
# Lower() both sides so matching is case-insensitive on Postgres too, matching MariaDB's
# default collation (MariaDB result is unchanged -- it already matches case-insensitively).
.where(
(iva.attribute == attribute) & (iva.attribute_value.isin([cstr(v) for v in attribute_values]))
(Lower(iva.attribute) == cstr(attribute).lower())
& (Lower(iva.attribute_value).isin([cstr(v).lower() for v in attribute_values]))
)
.where(iva.parent.isin(item_subquery))
.groupby(iva.parent)

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.controllers.item_variant import create_variant
from erpnext.tests.utils import ERPNextTestSuite
from erpnext.utilities.product import get_item_codes_by_attributes
class TestProduct(ERPNextTestSuite):
def test_get_item_codes_by_attributes_is_case_insensitive(self):
# get_item_codes_by_attributes matches Item Variant Attribute values. A raw equality is
# case-sensitive on Postgres, so a differently-cased filter value would miss variants that
# MariaDB (case-insensitive collation) matches. Lower() both sides keeps MariaDB unchanged and
# makes Postgres match too.
template = "_Test Variant Item"
variant = create_variant(template, {"Test Size": "Small"})
if not frappe.db.exists("Item", variant.name):
variant.insert()
self.addCleanup(frappe.delete_doc, "Item", variant.name, force=True)
# stored attribute value is "Small"; query with a different case
matches = get_item_codes_by_attributes({"Test Size": ["small"]}, template)
self.assertIn(variant.name, matches)