diff --git a/frappe/database/query.py b/frappe/database/query.py index 2f2fa5f749..a7c305ec19 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -155,8 +155,10 @@ FUNCTION_CALL_PATTERN = re.compile(r"^\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\(", flags=re. # - `tabTable Name`.`field` (spaces in table name) # - `tabTable-Field`.`field` (hyphens in table name) # - Any of above with aliases: ... as alias +# - Single-quoted aliases with colons (used by reportview child fields): +# - ... as 'Child:field' ALLOWED_FIELD_PATTERN = re.compile( - r"^(?:(`[\w\s-]+`|\w+)\.)?(`\w+`|\w+)(?:\s+as\s+(?:`[\w\s-]+`|\w+))?$", + r"^(?:(`[\w\s-]+`|\w+)\.)?(`\w+`|\w+)(?:\s+as\s+(?:`[\w\s-]+`|'[\w\s:-]+'|\w+))?$", flags=re.ASCII | re.IGNORECASE, ) @@ -963,7 +965,7 @@ class Engine: parts = re.split(r"\s+as\s+", field, flags=re.IGNORECASE) if len(parts) > 1: field_part = parts[0].strip() - alias = parts[1].strip().strip('`"') # Remove potential quotes from alias + alias = parts[1].strip().strip("`\"'") # Remove potential quotes from alias match = FIELD_PARSE_REGEX.match(field_part) @@ -1707,7 +1709,7 @@ class DynamicTableField: parts = re.split(r"\s+as\s+", field, flags=re.IGNORECASE) if len(parts) > 1: field_part = parts[0].strip() - alias = parts[-1].strip().strip('`"') # Get last part as alias + alias = parts[-1].strip().strip("`\"'") # Get last part as alias field = field_part # Use the part before alias for further parsing child_match = None diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index e52fd6ee5b..5110671578 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -1298,6 +1298,20 @@ class TestReportView(IntegrationTestCase): user.remove_roles("Blogger", "Website Manager") user.add_roles(*user_roles) + def test_reportview_child_table_alias(self): + from frappe.desk import reportview + + frappe.local.form_dict = frappe._dict( + { + "doctype": "DocType", + "fields": ["name", "`tabDocField`.`fieldname` as 'DocField:fieldname'"], + "limit": 1, + } + ) + response = reportview.get() + self.assertIn("DocField:fieldname", response["keys"]) + self.assertNotIn("'DocField:fieldname'", response["keys"]) + def test_reportview_get_aggregation(self): # test aggregation based on child table field frappe.local.request = frappe._dict()