diff --git a/frappe/database/query.py b/frappe/database/query.py index 9b71d307a3..8614601bfa 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -79,6 +79,44 @@ def _apply_date_field_filter_conversion(value, operator: str, doctype: str, fiel return value +def _apply_datetime_field_filter_conversion(between_values: tuple | list, doctype: str, field) -> tuple: + """Apply date to datetime conversion for Datetime fields with 'between' operator. + + Args: + between_values: Tuple/list of two values [from, to] for between filter + doctype: DocType name + field: Field name or pypika Field object + + Returns: + Tuple with dates expanded to datetime ranges for Datetime fields + """ + from frappe.model.db_query import _convert_type_for_between_filters + + # Extract field name + field_name = field + if "." in str(field): + field_name = field.split(".")[-1] + + # Skip querying meta for core doctypes to avoid recursion + if doctype in CORE_DOCTYPES: + df = None + else: + meta = frappe.get_meta(doctype) + df = meta.get_field(field_name) if meta else None + + # Standard datetime fields or Datetime fieldtype + if not (field_name in ("creation", "modified") or (df and df.fieldtype == "Datetime")): + return between_values + + from_val, to_val = between_values + + # Convert to datetime using db_query helper (handles strings, dates, datetimes) + from_val = _convert_type_for_between_filters(from_val, set_time=datetime.time()) + to_val = _convert_type_for_between_filters(to_val, set_time=datetime.time(23, 59, 59, 999999)) + + return (from_val, to_val) + + if TYPE_CHECKING: from frappe.query_builder import DocType @@ -487,12 +525,22 @@ class Engine: frappe.throw(_("Document cannot be used as a filter value")) _operator = operator + if _operator.lower() in ("timespan", "previous", "next"): + from frappe.model.db_query import get_date_range + + _value = get_date_range(_operator.lower(), _value) + _operator = "between" + # For Date fields with datetime values, convert to date to match db_query behavior if isinstance(_value, datetime.datetime) or ( isinstance(_value, list | tuple) and any(isinstance(v, datetime.datetime) for v in _value) ): _value = _apply_date_field_filter_conversion(_value, _operator, doctype or self.doctype, field) + # For Datetime fields with date values and 'between' operator, convert to datetime range to match db_query + if _operator.lower() == "between" and isinstance(_value, list | tuple) and len(_value) == 2: + _value = _apply_datetime_field_filter_conversion(_value, doctype or self.doctype, field) + if not _value and isinstance(_value, list | tuple | set): _value = ("",) diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py index 96e78990f3..e4d9150014 100644 --- a/frappe/tests/test_query.py +++ b/frappe/tests/test_query.py @@ -1902,6 +1902,30 @@ class TestQuery(IntegrationTestCase): # If we get here without PermissionError, the test passes self.assertIn(self.normalize_sql("GROUP BY `created_date`"), self.normalize_sql(sql)) + def test_between_datetime_expansion(self): + """Test that date strings are expanded to datetime ranges for Datetime fields with 'between' operator""" + # Test with creation field (standard datetime field) + query = frappe.qb.get_query( + "User", + filters={"creation": ["between", ["2025-12-01", "2025-12-01"]]}, + ) + sql = query.get_sql() + # Date strings should be expanded to datetime ranges + self.assertIn("2025-12-01 00:00:00", sql) + self.assertIn("2025-12-01 23:59:59", sql) + + def test_timespan_datetime_expansion(self): + """Test that timespan operator expands dates to datetime ranges for Datetime fields""" + query = frappe.qb.get_query( + "User", + filters={"creation": ["timespan", "last 7 days"]}, + ) + sql = query.get_sql() + # Timespan should expand dates to datetime ranges (start of first day, end of last day) + # Should have times like 00:00:00 and 23:59:59 + self.assertIn("00:00:00", sql) + self.assertIn("23:59:59", sql) + # This function is used as a permission query condition hook def test_permission_hook_condition(user):