mcpField
F
frappe-impl-reports

frappe-impl-reports

github.com/impertio-studio数据与数据库198110 次安装30 周前更新
代码仓库

关于 frappe-impl-reports

面向 Frappe 开发者的数据报表构建指南,介绍 Query Report、Script Report、Report Builder 和 Prepared Report,并涵盖图表、指标卡、仪表板及性能优化。

主要功能

  1. 01选择适合需求的报表类型
  2. 02构建 SQL 或脚本报表
  3. 03为报表添加图表和摘要
  4. 04配置 Prepared Report
  5. 05组合指标卡与仪表板并优化查询

适用场景

  1. 01通过 SQL 汇总业务记录
  2. 02为复杂报表添加图表和关键指标
  3. 03为大型数据集启用后台报表生成
  4. 04将图表和指标卡组合成仪表板

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/impertio-studio/frappe_claude_skill_package --skill frappe-impl-reports

安装后在 Claude Code 中输入/frappe-impl-reports确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
.gitkeep
anti-patterns.md
examples.md
workflows.md

详细介绍

Frappe 报表构建

快速参考

报表类型适用场景访问权限文件
查询报表简单的 SQL 查询仅系统管理员DocType 中的 SQL 或 .py
脚本报表复杂逻辑、图表管理员 + 开发模式.py + .js
报表构建器终端用户临时报表任何获准用户仅 UI
预生成报表大型数据集(>100k 行)与源报表相同后台任务

决策树:选择哪种报表类型?

Need a report?
├─ End user builds it themselves? → Report Builder
├─ Simple SQL with no Python logic? → Query Report
├─ Complex logic / charts / summary? → Script Report
│   └─ Dataset > 100k rows or timeout? → Add prepared_report = True
└─ Real-time KPI on workspace? → Number Card or Dashboard Chart

1. 创建脚本报表

文件结构
my_app/my_module/report/sales_summary/
├── sales_summary.json    # Report DocType definition
├── sales_summary.py      # Python: execute() function
└── sales_summary.js      # JavaScript: filters + config

始终通过 Desk 创建:Report > New > Script Report > 在开发模式下设置 "Is Standard = Yes"。

Python:execute() 函数
# sales_summary.py
import frappe
from frappe import _

def execute(filters=None):
    columns = get_columns()
    data = get_data(filters)
    chart = get_chart(data)
    report_summary = get_summary(data)
    return columns, data, None, chart, report_summary

def get_columns():
    return [
        {"fieldname": "customer", "label": _("Customer"), "fieldtype": "Link",
         "options": "Customer", "width": 200},
        {"fieldname": "total", "label": _("Total"), "fieldtype": "Currency",
         "options": "currency", "width": 120},
        {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Int", "width": 80},
        {"fieldname": "posting_date", "label": _("Date"), "fieldtype": "Date", "width": 100},
    ]

def get_data(filters):
    conditions = get_conditions(filters)
    return frappe.db.sql("""
        SELECT
            si.customer, SUM(si.grand_total) as total,
            SUM(si.total_qty) as qty, si.posting_date
        FROM `tabSales Invoice` si
        WHERE si.docstatus = 1 {conditions}
        GROUP BY si.customer
        ORDER BY total DESC
    """.format(conditions=conditions), filters, as_dict=True)

def get_conditions(filters):
    conditions = ""
    if filters.get("from_date"):
        conditions += " AND si.posting_date >= %(from_date)s"
    if filters.get("to_date"):
        conditions += " AND si.posting_date <= %(to_date)s"
    if filters.get("company"):
        conditions += " AND si.company = %(company)s"
    return conditions

返回值顺序(按位置排列——始终保持此顺序):

位置名称类型必填
1columnslist[dict]是
2datalist[dict] or list[list]是
3messagestr or None否
4chartdict or None否
5report_summarylist[dict] or None否
6skip_total_rowsbool否
JavaScript:筛选器
// sales_summary.js
frappe.query_reports["Sales Summary"] = {
    filters: [
        {
            fieldname: "company",
            label: __("Company"),
            fieldtype: "Link",
            options: "Company",
            default: frappe.defaults.get_user_default("company"),
            reqd: 1
        },
        {
            fieldname: "from_date",
            label: __("From Date"),
            fieldtype: "Date",
            default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
            reqd: 1
        },
        {
            fieldname: "to_date",
            label: __("To Date"),
            fieldtype: "Date",
            default: frappe.datetime.get_today(),
            reqd: 1
        },
        {
            fieldname: "customer_group",
            label: __("Customer Group"),
            fieldtype: "Link",
            options: "Customer Group",
            depends_on: "eval:doc.company"
        }
    ],
    formatter: function(value, row, column, data, default_formatter) {
        value = default_formatter(value, row, column, data);
        if (column.fieldname === "total" && data.total > 100000) {
            value = "<span style='color:green;font-weight:bold'>" + value + "</span>";
        }
        return value;
    }
};

2. 创建查询报表

查询报表使用原始 SQL。始终在 SQL 别名中使用旧版列格式:

SELECT
    `tabWork Order`.name AS "Work Order:Link/Work Order:200",
    `tabWork Order`.creation AS "Date:Date:120",
    `tabWork Order`.company AS "Company:Link/Company:150",
    `tabWork Order`.qty AS "Qty:Int:80",
    `tabWork Order`.grand_total AS "Total:Currency:120"
FROM `tabWork Order`
WHERE `tabWork Order`.docstatus = 1
ORDER BY `tabWork Order`.creation DESC

列格式:"标签:字段类型/选项:宽度"

在 WHERE 子句中使用 %(filter_name)s 表示筛选器变量。

3. 为报表添加图表

将图表字典作为 execute() 返回值的第 4 个元素:

def get_chart(data):
    labels = [d.customer for d in data[:10]]
    values = [d.total for d in data[:10]]
    return {
        "data": {
            "labels": labels,
            "datasets": [{"name": _("Revenue"), "values": values}]
        },
        "type": "bar",            # bar | line | pie | donut | percentage
        "colors": ["#7cd6fd"],
        "barOptions": {"stacked": False},  # for bar charts
        "height": 300
    }

图表类型:bar、line、pie、donut、percentage。

对于多数据集图表(例如,对比不同时段):

"datasets": [
    {"name": "2024", "values": [10, 20, 30]},
    {"name": "2025", "values": [15, 25, 35]}
]

4. 添加报表摘要

将摘要字典列表作为第 5 个元素返回:

def get_summary(data):
    total_revenue = sum(d.total for d in data)
    total_qty = sum(d.qty for d in data)
    return [
        {"value": total_revenue, "label": _("Total Revenue"),
         "datatype": "Currency", "currency": "USD",
         "indicator": "Green" if total_revenue > 0 else "Red"},
        {"value": total_qty, "label": _("Total Qty"),
         "datatype": "Int", "indicator": "Blue"},
        {"value": len(data), "label": _("Customers"),
         "datatype": "Int", "indicator": "Grey"}
    ]

指示器颜色:Green、Blue、Orange、Red、Grey。

5. 预生成报表

对于大型数据集上会超时的报表,在 .js 文件中添加:

frappe.query_reports["Heavy Report"] = {
    filters: [ /* ... */ ],
    prepared_report: true    // enables background generation
};

当 prepared_report: true 时,Frappe 会通过后台任务将报表加入队列。用户可以查看缓存结果,并按需重新生成。

6. 数字卡片

工作区仪表盘可使用三种数字卡片:

类型来源用途
文档类型DocType 聚合统计文档数量或总和
报表脚本报表/查询报表根据报表数据展示 KPI
自定义加入白名单的方法任意计算值
文档类型数字卡片

通过 Desk > Number Card 创建。设置 DocType、聚合函数(Count/Sum/Avg)和筛选条件。

基于报表的数字卡片

指向现有报表。卡片会显示第一行中的第一个数值列。

自定义方法数字卡片
# In your app, create a whitelisted method:
@frappe.whitelist()
def get_open_tickets():
    count = frappe.db.count("Issue", {"status": "Open"})
    return {"value": count, "fieldtype": "Int", "route_options": {"status": "Open"},
            "route": ["query-report", "Open Issues"]}

7. 仪表盘图表

通过 Desk > Dashboard Chart 创建,或在 fixtures 中以编程方式创建:

# hooks.py
fixtures = [
    {"dt": "Dashboard Chart", "filters": [["module", "=", "My Module"]]}
]

来源类型:报表、分组、自定义(加入白名单的方法)。

分组图表
{
    "chart_name": "Invoices by Status",
    "chart_type": "Group By",
    "document_type": "Sales Invoice",
    "group_by_type": "Count",
    "group_by_based_on": "status",
    "type": "Donut",
    "filters_json": "{\"docstatus\": 1}"
}

8. 构建仪表盘

仪表盘可组合多个图表和数字卡片:

{
    "name": "Sales Dashboard",
    "module": "Selling",
    "charts": [
        {"chart": "Monthly Revenue", "width": "Full"},
        {"chart": "Invoices by Status", "width": "Half"},
        {"chart": "Top Customers", "width": "Half"}
    ],
    "cards": [
        {"card": "Total Revenue"},
        {"card": "Open Orders"}
    ]
}

9. 性能优化

  • 始终为 WHERE/GROUP BY 中使用的列添加索引(frappe.model.utils.add_index)
  • 始终在 frappe.db.sql() 中使用 as_dict=True——使其与列字段名匹配
  • 绝不使用 SELECT *——明确指定列
  • 绝不要在报表循环中加载完整文档(frappe.get_doc)——请使用 SQL
  • 在 v14+ 中使用 frappe.qb(查询构建器)执行参数化查询
  • 对于超过 50k 行的报表,始终启用 prepared_report: true
  • 始终按 docstatus 筛选,以排除草稿/已取消的文档

10. 常见模式

日期范围筛选模式
if filters.get("from_date") and filters.get("to_date"):
    conditions += " AND posting_date BETWEEN %(from_date)s AND %(to_date)s"
多币种模式
{"fieldname": "amount", "label": _("Amount"), "fieldtype": "Currency",
 "options": "currency", "width": 120}
# "options": "currency" means use the row's "currency" field for formatting
分组并汇总模式
data = frappe.db.sql("""
    SELECT customer, COUNT(*) as count, SUM(grand_total) as total
    FROM `tabSales Invoice`
    WHERE docstatus = 1 {conditions}
    GROUP BY customer WITH ROLLUP
""".format(conditions=conditions), filters, as_dict=True)

另请参阅

评论

支持 Markdown