Using legacy string format in Script Reports or dict format in Query Report SQL aliases.
# In a Script Report .py file — legacy string format does NOT work here
def execute(filters=None):
columns = [
"Customer:Link/Customer:200",
"Amount:Currency:120"
]# Script Reports ALWAYS use dict format
def execute(filters=None):
columns = [
{"fieldname": "customer", "label": _("Customer"),
"fieldtype": "Link", "options": "Customer", "width": 200},
{"fieldname": "amount", "label": _("Amount"),
"fieldtype": "Currency", "width": 120}
]Rule: ALWAYS use dict format for Script Reports. Legacy string format is ONLY for Query Report SQL aliases.
Returning None for columns or data crashes the report renderer.
def execute(filters=None):
if not filters.get("company"):
return None, None # Crashes!def execute(filters=None):
if not filters.get("company"):
return [], [] # Safe empty returnRule: ALWAYS return [] for empty columns and data — NEVER None.
Building SQL with f-strings or .format() using user-supplied filter values.
def get_data(filters):
# DANGEROUS — SQL injection vulnerability
return frappe.db.sql(f"""
SELECT name FROM `tabSales Order`
WHERE customer = '{filters.get("customer")}'
""", as_dict=True)def get_data(filters):
return frappe.db.sql("""
SELECT name FROM `tabSales Order`
WHERE customer = %(customer)s
""", filters, as_dict=True)Rule: ALWAYS use %(param)s placeholders and pass filters dict to frappe.db.sql. NEVER interpolate user input into SQL strings.
Querying submitted documents without filtering by docstatus, which returns Draft and Cancelled documents.
data = frappe.db.sql("""
SELECT name, grand_total FROM `tabSales Invoice`
WHERE customer = %(customer)s
""", filters, as_dict=True)data = frappe.db.sql("""
SELECT name, grand_total FROM `tabSales Invoice`
WHERE docstatus = 1
AND customer = %(customer)s
""", filters, as_dict=True)Rule: ALWAYS include docstatus = 1 when querying submitted (final) documents. Use docstatus < 2 to include both Draft and Submitted but exclude Cancelled.
Chart labels array and datasets[].values arrays have different lengths, causing rendering errors or blank charts.
chart = {
"data": {
"labels": ["Jan", "Feb", "Mar"], # 3 labels
"datasets": [
{"name": "Revenue", "values": [100, 200]} # 2 values — MISMATCH
]
},
"type": "bar"
}labels = [row.month for row in data]
values = [flt(row.amount) for row in data]
chart = {
"data": {
"labels": labels, # Same source, guaranteed equal length
"datasets": [
{"name": _("Revenue"), "values": values}
]
},
"type": "bar"
}Rule: ALWAYS derive labels and values from the same data source to guarantee equal length.
Omitting width in column definitions causes columns to render too narrow or overlap.
columns = [
{"fieldname": "customer", "label": _("Customer"),
"fieldtype": "Link", "options": "Customer"}
# No width — renders poorly
]columns = [
{"fieldname": "customer", "label": _("Customer"),
"fieldtype": "Link", "options": "Customer", "width": 200}
]Rule: ALWAYS specify width (in pixels) for every column definition.
Creating a report without setting Reference DocType. The report is only visible to Administrators.
Report document with no Reference DocType set.
ALWAYS set Reference DocType to the primary DocType the report queries. This controls:
- Who can see the report (users with read permission on that DocType)
- Where the report appears in the sidebar
Rule: ALWAYS set Reference DocType on every report.
Confusing frappe.query_reports (the registry object) with frappe.query_report (the active instance).
// This accesses the configuration object, NOT the running instance
frappe.query_reports["My Report"].refresh(); // Does nothing// This accesses the active report instance
frappe.query_report.refresh(); // Actually refreshes
frappe.query_report.get_filter_value("company"); // Gets filter valueRule: Use frappe.query_reports["Name"] ONLY for defining configuration. Use frappe.query_report (singular) for runtime operations.
DateRange filter returns a list [from_date, to_date], not individual date values. Using it directly in SQL fails.
def execute(filters=None):
# filters.date_range = ["2024-01-01", "2024-03-31"]
data = frappe.db.sql("""
SELECT name FROM `tabSales Order`
WHERE transaction_date BETWEEN %(date_range)s -- FAILS
""", filters, as_dict=True)def execute(filters=None):
if filters.get("date_range"):
filters["from_date"] = filters["date_range"][0]
filters["to_date"] = filters["date_range"][1]
data = frappe.db.sql("""
SELECT name FROM `tabSales Order`
WHERE transaction_date BETWEEN %(from_date)s AND %(to_date)s
""", filters, as_dict=True)Rule: ALWAYS unpack DateRange filters into separate from_date and to_date keys before using in SQL.
Hardcoding English strings in labels without _() or __() makes reports untranslatable.
columns = [
{"fieldname": "customer", "label": "Customer", ...} # Not translatable
]
report_summary = [
{"value": total, "label": "Total Revenue", ...} # Not translatable
]columns = [
{"fieldname": "customer", "label": _("Customer"), ...}
]
report_summary = [
{"value": total, "label": _("Total Revenue"), ...}
]// In JS files
{ fieldname: "customer", label: __("Customer"), ... }Rule: ALWAYS wrap label strings with _() in Python and __() in JavaScript.
Forgetting as_dict=True when columns use dict format. Data comes back as tuples instead of dicts, and fieldnames do not map to column definitions.
data = frappe.db.sql("""
SELECT customer, grand_total FROM `tabSales Order`
""", filters) # Returns list of tuplesdata = frappe.db.sql("""
SELECT customer, grand_total FROM `tabSales Order`
""", filters, as_dict=True) # Returns list of dictsRule: When using dict-format columns, ALWAYS use as_dict=True in frappe.db.sql() so that fieldname keys in columns match the dict keys in data rows.
Python string formatting interprets % in SQL date functions as format specifiers.
frappe.db.sql("""
SELECT DATE_FORMAT(posting_date, '%Y-%m') as month
FROM `tabSales Invoice`
""", filters, as_dict=True)
# Error: not enough arguments for format stringfrappe.db.sql("""
SELECT DATE_FORMAT(posting_date, '%%Y-%%m') as month
FROM `tabSales Invoice`
""", filters, as_dict=True)
# Double %% escapes the percent signRule: ALWAYS use %% to escape percent signs in SQL when using frappe.db.sql with parameters.