$npx -y skills add lubusIN/frappe-skills --skill frappe-reportsCreate reports in Frappe including Report Builder, Query Reports (SQL), and Script Reports (Python + JS). Use when building data analysis views, dashboards, or custom reporting features.
| 1 | # Frappe Reports |
| 2 | |
| 3 | Build reports using Report Builder, Query Reports (SQL), or Script Reports (Python + JS). |
| 4 | |
| 5 | ## When to use |
| 6 | |
| 7 | - Creating data analysis or summary reports |
| 8 | - Building SQL-based query reports |
| 9 | - Implementing complex reports with Python logic and JS UI |
| 10 | - Adding custom filters, formatters, and charts to reports |
| 11 | - Creating printable report formats |
| 12 | |
| 13 | ## Inputs required |
| 14 | |
| 15 | - Report purpose and data requirements |
| 16 | - Source DocType(s) for the report |
| 17 | - Filter requirements |
| 18 | - Column definitions (fields, types, formatting) |
| 19 | - Whether report is standard (app-bundled) or custom (site-specific) |
| 20 | |
| 21 | ## Procedure |
| 22 | |
| 23 | ### 0) Choose report type |
| 24 | |
| 25 | | Type | Complexity | Code Required | Best For | |
| 26 | |------|-----------|---------------|----------| |
| 27 | | Report Builder | Low | None | Simple field selection, grouping, sorting | |
| 28 | | Query Report | Medium | SQL only | Direct SQL queries, joins, aggregations | |
| 29 | | Script Report | High | Python + JS | Complex logic, computed fields, dynamic filters | |
| 30 | |
| 31 | ### 1) Report Builder |
| 32 | |
| 33 | Create via UI with no code: |
| 34 | 1. Navigate to the Report list → New Report |
| 35 | 2. Select Reference DocType |
| 36 | 3. Choose Report Type = "Report Builder" |
| 37 | 4. Add columns, filters, sorting, and grouping via the builder UI |
| 38 | |
| 39 | ### 2) Query Report |
| 40 | |
| 41 | Reports using raw SQL queries: |
| 42 | |
| 43 | 1. Create Report → Type = "Query Report" |
| 44 | 2. Set Reference DocType (controls permissions) |
| 45 | 3. Write SQL query |
| 46 | |
| 47 | ```sql |
| 48 | SELECT |
| 49 | `tabSales Order`.name AS "Sales Order:Link/Sales Order:200", |
| 50 | `tabSales Order`.customer AS "Customer:Link/Customer:200", |
| 51 | `tabSales Order`.transaction_date AS "Date:Date:120", |
| 52 | `tabSales Order`.grand_total AS "Grand Total:Currency:150", |
| 53 | `tabSales Order`.status AS "Status:Data:100" |
| 54 | FROM `tabSales Order` |
| 55 | WHERE `tabSales Order`.docstatus = 1 |
| 56 | {% if filters.company %} |
| 57 | AND `tabSales Order`.company = %(company)s |
| 58 | {% endif %} |
| 59 | {% if filters.from_date %} |
| 60 | AND `tabSales Order`.transaction_date >= %(from_date)s |
| 61 | {% endif %} |
| 62 | ORDER BY `tabSales Order`.transaction_date DESC |
| 63 | ``` |
| 64 | |
| 65 | **Column format in SELECT**: `"Label:Fieldtype/Options:Width"` |
| 66 | |
| 67 | | Fieldtype | Example | |
| 68 | |-----------|---------| |
| 69 | | Link | `"Customer:Link/Customer:200"` | |
| 70 | | Currency | `"Amount:Currency:150"` | |
| 71 | | Date | `"Date:Date:120"` | |
| 72 | | Int | `"Quantity:Int:100"` | |
| 73 | | Data | `"Status:Data:100"` | |
| 74 | |
| 75 | **Filter variables**: Use `%(filter_name)s` for parameterized queries. |
| 76 | |
| 77 | ### 3) Script Report (standard) |
| 78 | |
| 79 | For app-bundled reports with full Python + JS control: |
| 80 | |
| 81 | **Create the report structure:** |
| 82 | ``` |
| 83 | my_app/ |
| 84 | └── my_module/ |
| 85 | └── report/ |
| 86 | └── sales_summary/ |
| 87 | ├── sales_summary.json # Report metadata |
| 88 | ├── sales_summary.py # Python data logic |
| 89 | └── sales_summary.js # JS filters and UI |
| 90 | ``` |
| 91 | |
| 92 | **Python script** (`sales_summary.py`): |
| 93 | |
| 94 | ```python |
| 95 | import frappe |
| 96 | from frappe import _ |
| 97 | |
| 98 | def execute(filters=None): |
| 99 | columns = get_columns() |
| 100 | data = get_data(filters) |
| 101 | chart = get_chart(data) |
| 102 | return columns, data, None, chart |
| 103 | |
| 104 | def get_columns(): |
| 105 | return [ |
| 106 | { |
| 107 | "label": _("Customer"), |
| 108 | "fieldname": "customer", |
| 109 | "fieldtype": "Link", |
| 110 | "options": "Customer", |
| 111 | "width": 200 |
| 112 | }, |
| 113 | { |
| 114 | "label": _("Total Orders"), |
| 115 | "fieldname": "total_orders", |
| 116 | "fieldtype": "Int", |
| 117 | "width": 120 |
| 118 | }, |
| 119 | { |
| 120 | "label": _("Total Amount"), |
| 121 | "fieldname": "total_amount", |
| 122 | "fieldtype": "Currency", |
| 123 | "width": 150 |
| 124 | }, |
| 125 | { |
| 126 | "label": _("Average Order"), |
| 127 | "fieldname": "avg_order", |
| 128 | "fieldtype": "Currency", |
| 129 | "width": 150 |
| 130 | } |
| 131 | ] |
| 132 | |
| 133 | def get_data(filters): |
| 134 | conditions = get_conditions(filters) |
| 135 | |
| 136 | data = frappe.db.sql(""" |
| 137 | SELECT |
| 138 | customer, |
| 139 | COUNT(name) as total_orders, |
| 140 | SUM(grand_total) as total_amount, |
| 141 | AVG(grand_total) as avg_order |
| 142 | FROM `tabSales Order` |
| 143 | WHERE docstatus = 1 {conditions} |
| 144 | GROUP BY customer |
| 145 | ORDER BY total_amount DESC |
| 146 | """.format(conditions=conditions), filters, as_dict=True) |
| 147 | |
| 148 | return data |
| 149 | |
| 150 | def get_conditions(filters): |
| 151 | conditions = "" |
| 152 | if filters.get("company"): |
| 153 | conditions += " AND company = %(company)s" |
| 154 | if filters.get("from_date"): |
| 155 | conditions += " AND transaction_date >= %(from_date)s" |
| 156 | if filters.get("to_date"): |
| 157 | conditions += " AND transaction_date <= %(to_date)s" |
| 158 | return conditions |
| 159 | |
| 160 | def get_chart(data): |
| 161 | if not data: |
| 162 | return None |
| 163 | |
| 164 | return { |
| 165 | "data": { |
| 166 | "labels": [d.customer for d in data[:10]], |
| 167 | "datasets": [{ |
| 168 | "name": _("Total Amount"), |
| 169 | "values": [d.total_amount for d in data[:10]] |