Files
erpnext/erpnext/utilities/query.py
Mihir Kandoi 785c34e0ad refactor: parse native JSON request args in utilities/query.py
Use frappe.parse_json instead of json.loads so the whitelisted endpoints
accept native JSON types (list/dict/bool) in addition to JSON strings.
2026-06-24 20:37:35 +05:30

96 lines
4.0 KiB
Python

# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
"""Query-builder helpers for permission & filter conditions.
These are ERPNext-local because they are only consumed by ERPNext. They return
``pypika`` criteria (rather than the raw SQL strings produced by
``frappe.desk.reportview.get_match_cond`` / ``get_filters_cond``) so the conditions
can be applied to any ``frappe.qb`` query via ``.where(...)`` — including joins and
aliased queries where the permission-checked doctype is not the single base of
``frappe.qb.get_query``.
They are thin wrappers over ``frappe.database.query.Engine`` (``get_permission_conditions``
/ ``apply_filters``), which pre-date this code. Where the permission-checked doctype *is*
the base of the query, prefer ``frappe.qb.get_query(doctype, ignore_permissions=False)``
directly instead of these helpers.
"""
import json
import frappe
from frappe import _
def get_match_conditions_qb(doctype, table=None, user=None):
"""Return user-permission match conditions for ``doctype`` as query-builder criteria.
Query-builder equivalent of ``frappe.desk.reportview.get_match_cond`` /
``build_match_conditions`` (which return raw SQL strings). Returns a list of pypika
criteria (0 or 1 elements) covering role permissions, user permissions, sharing and the
if-owner constraint as well as ``permission_query_conditions`` hooks/server scripts.
Args:
doctype: doctype to build permission conditions for.
table: pypika table the conditions should reference. Defaults to
``frappe.qb.DocType(doctype)``.
user: user to evaluate permissions for. Defaults to the session user.
"""
from frappe.database.query import Engine
engine = Engine()
engine.get_query(doctype, user=user, ignore_permissions=False, db_query_compat=True)
condition = engine.get_permission_conditions(doctype, table or engine.table)
return [condition] if condition is not None else []
def get_filter_conditions_qb(doctype, filters, ignore_permissions=None):
"""Return ``filters`` for ``doctype`` as a list of query-builder criteria.
Query-builder equivalent of ``frappe.desk.reportview.get_filters_cond`` (which returns a
raw SQL string). Accepts the standard frappe filter forms (dict, or list of
``[doctype, field, op, value]`` rows) and returns pypika criteria that can be applied to
any ``frappe.qb`` query via ``.where(...)``.
"""
if not filters:
return []
from pypika.terms import Criterion
# A pypika Criterion is already a usable condition; apply_filters would route it straight to
# the query and never populate `collect`, silently returning []. Hand it back as-is instead.
if isinstance(filters, Criterion):
return [filters]
filters = frappe.parse_json(filters)
if isinstance(filters, dict):
# Mirror get_filters_cond's dict normalization: a string value prefixed with "!" means
# "not equal" (e.g. {"enabled": "!1"} -> enabled != "1"). apply_filters' dict path would
# otherwise treat "!1" as a literal value and emit `enabled = "!1"`.
filters = {
field: ("!=", value[1:]) if isinstance(value, str) and value.startswith("!") else value
for field, value in filters.items()
}
from frappe.database.query import Engine
engine = Engine()
engine.get_query(doctype, ignore_permissions=ignore_permissions, db_query_compat=True)
criteria = []
engine.apply_filters(filters, collect=criteria)
return criteria
def get_event_conditions_qb(doctype, filters=None):
"""Return user-permission match conditions + ``filters`` for event/calendar queries.
Query-builder equivalent of ``frappe.desk.calendar.get_event_conditions(..., as_qb=True)``:
a list of pypika criteria suitable for applying to a ``frappe.qb`` query via ``.where(...)``
(e.g. calendar feeds that join across multiple doctypes).
"""
if not frappe.has_permission(doctype):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
return get_match_conditions_qb(doctype) + get_filter_conditions_qb(doctype, filters)