diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index eecacbd790..124843dfd9 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -888,7 +888,11 @@ from {tables} if value is None: values = f.value or "" if isinstance(values, str): - values = values.split(",") + try: + parsed = json.loads(values) + values = parsed if isinstance(parsed, list) else [parsed] + except ValueError: + values = values.split(",") fallback = "''" value = [frappe.db.escape((cstr(v) or "").strip(), percent=False) for v in values] diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 40f183450f..95ac66013f 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -203,7 +203,9 @@ frappe.ui.Filter = class { this._filter_value_set = Promise.resolve(); if (["in", "not in"].includes(condition) && Array.isArray(value)) { - value = value.join(","); + value = value.some((v) => String(v).includes(",")) + ? JSON.stringify(value) + : value.join(","); } if (Array.isArray(value)) { @@ -485,7 +487,12 @@ frappe.ui.filter_utils = { } } else if (["in", "not in"].includes(condition)) { if (val) { - val = val.split(",").map((v) => strip(v)); + try { + const parsed = JSON.parse(val); + val = Array.isArray(parsed) ? parsed : [String(parsed)]; + } catch { + val = val.split(",").map((v) => strip(v)); + } } } else if (frappe.boot.additional_filters_config[condition]) { val = field.value || val; diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 8246da7d8b..c20b8aa9c1 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -271,6 +271,33 @@ class TestDBQuery(IntegrationTestCase): result in DatabaseQuery("DocType").execute(filters={"name": ["not in", "DocType,DocField"]}) ) + def test_in_filter_json_encoded_values(self): + # JSON-encoded list string should work the same as comma-separated + for result in [{"name": "DocType"}, {"name": "DocField"}]: + self.assertTrue( + result + in DatabaseQuery("DocType").execute(filters={"name": ["in", '["DocType", "DocField"]']}) + ) + + # Values containing commas must not be split + todo = frappe.get_doc( + doctype="ToDo", description="Test, With Comma", allocated_to="Administrator" + ).insert() + try: + results = DatabaseQuery("ToDo").execute( + filters={"description": ["in", '["Test, With Comma"]']}, + fields=["description"], + ) + self.assertIn({"description": "Test, With Comma"}, results) + + results_split = DatabaseQuery("ToDo").execute( + filters={"description": ["in", "Test, With Comma"]}, + fields=["description"], + ) + self.assertNotIn({"description": "Test, With Comma"}, results_split) + finally: + frappe.delete_doc("ToDo", todo.name) + def test_string_as_field(self): self.assertEqual( frappe.get_all("DocType", as_list=True), frappe.get_all("DocType", fields="name", as_list=True) diff --git a/frappe/types/filter.py b/frappe/types/filter.py index acf9369322..197577faa2 100644 --- a/frappe/types/filter.py +++ b/frappe/types/filter.py @@ -1,3 +1,4 @@ +import json import textwrap from collections import defaultdict from collections.abc import Generator, Iterable, Mapping, Sequence @@ -110,7 +111,11 @@ class FilterTuple(_FilterTuple): # soundness if operator in ("in", "not in") and isinstance(value, str): - value = value.split(",") + try: + parsed = json.loads(value) + value = parsed if isinstance(parsed, list) else value.split(",") # type: ignore[assignment] + except ValueError: + value = value.split(",") _value: Value if isinstance(value, _InputValue):