fix(pos): return raw Item Group names from get_item_groups (double-escape regression)

The Postgres-portability change moved the POS item-group filters to the query builder
(item.item_group.isin(...)) and frappe.get_all(["name","in",...]), which escape values
once. get_item_groups() still pre-escaped each name with frappe.db.escape(), so the
names were escaped TWICE -> `item_group IN ('''Products''')`, matching nothing. Any POS
Profile that restricts item groups returned ZERO items, on both MariaDB and Postgres.

Return raw names; the parameterized callers escape them correctly. (get_parent_item_group
also returned the quoted literal before this fix.) Add a regression test: a POS Profile
restricted to an item group must still surface that group's items — it returns 0 before
the fix and passes after, on both engines.
This commit is contained in:
Mihir Kandoi
2026-06-22 18:52:51 +05:30
parent a120bf8363
commit 32216bd75b
2 changed files with 31 additions and 2 deletions

View File

@@ -242,15 +242,18 @@ def get_item_groups(pos_profile):
for data in pos_profile.get("item_groups"):
item_groups.extend(
[
"%s" % frappe.db.escape(d.name)
d.name
for d in get_child_nodes("Item Group", data.item_group)
if not permitted_item_groups or d.name in permitted_item_groups
]
)
if not item_groups and permitted_item_groups:
item_groups = ["%s" % frappe.db.escape(d) for d in permitted_item_groups]
item_groups = list(permitted_item_groups)
# Return raw Item Group names; the callers parameterize them via the query builder
# (item_group.isin(...)) / frappe.get_all, which escapes them once. Pre-escaping here would
# double-escape (item_group IN ('''X''')) and match nothing.
return list(set(item_groups))

View File

@@ -102,6 +102,32 @@ class TestPointOfSaleGetItems(ERPNextTestSuite):
item_codes = self._get_item_codes(self.item_code)
self.assertNotIn(self.item_code, item_codes)
def test_pos_profile_item_group_restriction_returns_items(self):
# get_item_groups() returns Item Group names that the callers parameterize via
# item.item_group.isin(...) / frappe.get_all, which escapes them once. The names must be RAW;
# pre-escaping them double-escaped to `item_group IN ('''X''')`, so a POS Profile restricting
# item groups matched nothing and returned ZERO items on both engines.
from erpnext.accounts.doctype.pos_profile.pos_profile import get_item_groups
restricted = make_pos_profile(name="_Test POS Profile IG Restricted")
restricted.append("item_groups", {"item_group": self.item_group})
restricted.save()
# the helper must hand back the real (raw) group name, not an escaped literal
self.assertIn(self.item_group, get_item_groups(restricted.name))
# end-to-end: the restricted profile must still surface its in-group items
result = get_items(
start=0,
page_length=100,
price_list="Standard Selling",
item_group=self.item_group,
pos_profile=restricted.name,
search_term="",
)
items = result["items"] if isinstance(result, dict) else result
self.assertIn(self.item_code, [row.get("item_code") for row in items])
def test_non_sales_item_is_excluded(self):
# is_sales_item == 1 is part of the converted WHERE clause.
frappe.db.set_value("Item", self.item_code, "is_sales_item", 0)