库存价值对比

1. 这份报告是什么?

ERPNext 中的每一笔库存交易都会在库存台账中增加一行记录。每一行都会存储几个数字:

  • 交易后的结存数量
  • 该交易引起的库存价值变动
  • 计价费率
  • 结存库存价值

有时候这些数字会出错。常见原因包括:

  • 同一物料和仓库有大量倒填日期的记录
  • 重算失败或从未运行过
  • 旧版本的 bug 虽然已经修复,但错误的值还留在台账里
  • 对库存台账做了自定义修改

出现这种情况时,报告中的库存价值就是错的,而且库存价值可能和财务账目对不上。

库存计价对比报告可以找出这些问题。它会从每个物料和仓库的第一条记录开始,逐条检查台账,算出每个数字应该是多少。然后把实际值和预期值并排放在一起。如果两者不一致,该行就会显示为差异。

在同一份报告中,你可以用以下两种方式之一来修复差异:

  • 方式一:重算
  • 方式二:调整分录

本指南将介绍这两种方式,以及各自适用的场景。

2. 打开报告

前往库存 > 报表 > 库存计价对比,或者在搜索栏中搜索”库存计价对比”。

筛选条件

筛选条件 作用
公司 必填。要检查的公司。
开始日期 / 结束日期 只显示这两个日期之间的记录。报告仍然会从第一条记录开始读取台账,以确保数字准确。
物料 / 物料组 / 仓库 只检查这些物料或仓库。父级仓库包含其子仓库。
显示 每个物料-仓库的第一条差异(默认):只显示每个物料和仓库的第一条错误记录。通常这就够了,因为一条错误记录会导致后面所有记录都出错。
所有差异:显示每一条错误记录。
所有记录:显示每一条记录,无论对错。
忽略低于此值的差异 低于此金额的小额舍入差异会被忽略。默认值为 0.01。
显示已由调整分录结清的差异 已被调整分录结清的差异默认隐藏。勾选此项可重新显示它们。

3. 阅读报告

每一行是一条库存台账记录。各列按三列一组排列:实际值、预期值以及两者之间的差异。

实际值 预期值 差异 含义
(A) 结存数量 (B) 预期结存数量 A – B 此记录后的库存数量
(C) 库存价值变动 (D) 预期库存价值变动 C – D 此记录增加或减少了多少库存价值
(E) 计价费率 (F) 预期计价费率 E – F 此记录后的单位费率
(G) 结存库存价值 (H) 预期结存库存价值 G – H 此记录后的库存总价值

差异以红色显示。

其他有用的列:

  • 预期值依据:报告是如何算出预期值的。例如”先进先出队列”、”移动平均”、”批次平均”、”序列号收货费率”、”标准成本”或”已对账的数量和费率”。
  • 已调整方式:已经结清此差异的调整分录(如果有的话)。
  • 待处理重算:已为此物料和仓库排队或正在运行的库存重算任务。

修复之前请注意:先查看某个物料的第一条差异,打开它的凭证。确认预期值对你来说是合理的。如果预期值看起来不对,请不要修复该记录,先向你的 ERPNext 合作伙伴或支持团队报告。


4. 选择要修复的内容

两个修复按钮都在报告顶部的修复差异下方。

  • 如果你勾选了一些行,只修复这些行。
  • 如果你没有勾选任何行,报告中显示的所有差异都会被修复。

对于每个物料和仓库,修复总是从最早被选中的差异开始。


5. 方式一:重算

它的作用

重新过账会为每个选中的物料和仓库各生成一条重新过账物料估值,从其首次出现差异的日期开始。重新过账在后台运行。它会从该日期到今天重新计算每一条库存台账分录,并修正库存价值及其背后的会计分录。

重新过账后,台账从一开始就是正确的,就好像错误从未发生过一样。

步骤

  1. 选择要修复的行,或者不选任何行以修复显示的全部内容。
  2. 点击修复差异 > 重新过账。
  3. 会打开一个预览。它显示:
  • 将要重新过账的每个物料和仓库,以及其起始日期
  • 任何已经有待处理重新过账的物料和仓库。这些会被排除在外。
  • 如果重新过账从上一个财政年度开始,会显示警告
  1. 点击是继续。
  2. 会显示一条消息,说明哪些重新过账已排队及其链接,以及哪些无法执行及原因。
  3. 等待重新过账完成。你可以在重新过账物料估值列表中跟踪它们。
  4. 再次运行报表。差异应该已经消失。

优点

  • 从根源上解决问题。 从首次差异往后的每一条分录都会被修正,因此所有新旧报表都会显示正确的值。
  • 历史记录干净。 不会向台账添加额外交易。过去日期的库存和会计报表也会变得正确。
  • 对后续分录没有限制。 你可以像往常一样继续录入倒填日期的交易。
  • 无需手工记账。 你不需要差异科目。原始凭证的会计分录会被修正。

缺点

  • 会改变过去的数字。 如果首次差异发生在较早期间,该期间的期末库存、利润和资产负债表数字可能会改变。如果这些数字已经申报、审计或报告给税务机关,这就会成为问题。
  • 可能很慢。 交易很多的物料,或者起始日期很久远,重新过账可能需要很长时间。运行时还会增加系统负载。
  • 可能被阻止。 如果该期间已关闭,例如通过会计期间或期间关账凭证关闭,或者该日期之前的账户已冻结,则可能不允许重新过账。
  • 可能扩散得比预期更广。 更改发出物料的价值也会更改使用该库存的任何内容的价值,例如制造品或调拨到另一个仓库的物料。

6. 选项 2:调整分录

它的作用

调整分录会从你选择的日期开始修复库存,而不触碰该日期之前的任何内容。

在该日期,它会按台账当前持有的(错误)价值取出每个选中物料和仓库的库存,并按正确的数量和价值放回。序列号按各自费率返回,批次按各自费率返回,其余库存逐层返回,以便之后 FIFO 和移动平均保持正确。

价值差异会记入差异科目。默认情况下,这是公司的库存调整科目。

调整分录是一种用途为“调整分录”的库存对账。

步骤

  1. 选择要修复的行,或者不选任何行以修复显示的全部内容。
  2. 点击修复差异 > 调整分录。
  3. 会打开一个对话框。填写:
  • 调整日期和调整时间:库存应从该日期起正确的日期。常见选择是当前财政年度的第一天,或今天。
  • 差异科目(可选):价值差异记入的位置。留空则使用公司的库存调整科目。
  1. 点击创建。
  2. 会打开一个草稿库存对账。检查其行:
  • 当前数量 / 当前金额:台账现在持有的内容
  • 数量 / 估值费率 / 金额:它应该持有的内容
  • 差异金额:记入差异科目的金额
  1. 提交它。
  2. 再次运行报表。该日期之前的差异现在会被隐藏。勾选显示由调整分录结清的差异即可查看它们。

不要编辑这些行。 它们必须与库存应有的内容完全一致。如果你更改它们,将会收到错误,并且必须从报表重新创建调整分录。

优点

  • 过去的数字保持不变。 调整日期之前的任何内容都不会改变。已申报的纳税申报表、已审计的账目和已关闭的期间都是安全的。
  • 快速。 它只是一笔交易。即使是有多年历史的物料,也不会重新过账任何内容。
  • 易于查看和审计。 更正是带有明确金额的一张单据,并记入你选择的科目。你的会计可以准确看到更正了什么以及何时更正。
  • 在重新过账无法使用时也可用。 当旧期间已关闭或冻结时可以使用它。
  • 可以撤销。 取消调整分录会恢复库存到之前的状态。

缺点

  • 旧报表仍然错误。 调整日期之前的库存和会计报表仍然显示旧的、错误的值。只有从调整日期开始的值才是正确的。

  • 阻止倒填日期的分录。 提交后,该物料和仓库,或相同序列号或批次的任何早于调整日期的库存交易都无法过账或取消。要过账一笔,必须先取消调整分录。

  • 需要手动会计步骤。 差额会记入差异科目。您的会计可能需要审核或解释它。

  • 并非在所有情况下都适用。 物料和仓库会在以下情况下被排除,并附上原因:

    • 其库存数量和值在该日期已经正确
    • 其库存在该日期应为负数
    • 其某些序列号或批次在该日期应库存不足
    • 其第一个差异在调整日期当天或之后
    • 到该日期为止没有任何库存交易

    这些情况需要改用重新过账。


7. 并排比较

重新过账 调整分录
变更内容 从第一个差异到今天的所有分录 仅从调整日期起的库存
过去的报表 变为正确 保持原样(错误)
已关闭或已申报的期间 可能变更,或可能被阻止 不受影响
速度 历史较长时较慢,在后台运行 快速,单个单据
台账中的新单据 无 有,一个库存对账单
会计 原始会计分录入被更正 差额记入差异科目
之后的倒填日期分录 允许 调整日期之前被阻止
负库存 / 序列号或批次不足 有效 不支持,请使用重新过账
如何撤销 困难,旧值已丢失 取消调整分录

8. 我应该使用哪个选项?

在以下情况下使用重新过账:

  • 第一个差异在当前财政年度,或在一个尚未申报或关闭的期间
  • 您希望每一份报表,包括旧报表,都正确
  • 您仍然需要为这些物料过账倒填日期的分录
  • 调整分录提示该物料无法调整

在以下情况下使用调整分录:

  • 第一个差异在已关闭、已审计或已申报的过去财政年度
  • 重新过账太慢,或被已关闭的期间阻止
  • 您只需要库存从现在起正确,并且可以接受旧报表保持原样

一种常见做法:

  1. 重新过账当前财政年度内的差异。
  2. 对于始于较早、已关闭年度的差异,制作一笔日期为当前财政年度第一天的调整分录。请先与您的会计讨论。

9. 良好实践

  • 在修复大量物料之前,进行备份。
  • 在修复所有内容之前,手动检查几个物料。确保预期值是正确的。
  • 分成小组修复,例如一次一个物料组或一个仓库。
  • 每次修复后再次运行报表,确认差异已消失。
  • 在重新过账到过去财政年度之前,或在记入大额调整分录之前,与您的会计沟通。

已取消凭证的库存台账分录

有时候,一张凭证已经被取消了,但它的库存分类账条目仍然没有被取消。这可能是由于自定义设置或系统核心中的旧问题导致的。

此报告显示所有此类库存分类账条目,即凭证已经取消,但库存分类账条目仍然处于活动状态。

你可以使用此报告来修复这些条目。

选择你想要修复的条目,然后点击“取消库存分类账条目”。系统将取消所选的库存分类账条目,并为相同的物料和仓库创建重新过账条目。

取消和重新过账完成后,你的库存余额可能会发生变化。

示例

假设你的库存中有50 个水瓶。

你发现了一条关于5 个水瓶的错误库存分类账条目,并使用“取消库存分类账条目”进行了修复。

修复和重新过账完成后,你的库存将显示为45 个水瓶,而不是 50 个。

模块设置

Nova Industries 是本指南中虚构的一家电子产品制造商和分销商。其销售团队负责准备报价单,仓库负责发货,工厂负责组装部分产品,财务团队负责记录每一笔结果。当公司推出一项维修服务时,管理员必须决定在哪里配置这个新流程。全站安全设置、销售设置和库存设置看起来都像是”系统设置”,但它们解决的问题各不相同。

模块设置是一张地图,指引管理员找到每个业务流程所拥有的设置。它帮助 Nova 在不意外更改登录安全、会计规则或其他模块的情况下,调整销售订单的行为方式。用它找到正确的设置页面,然后在实际使用该设置的交易中进行测试。

模块设置是如何组织的

设置页面 控制内容 Nova Industries 示例
会计设置 会计验证、发票、递延会计、维度以及期末处理行为 财务要求在提交销售订单前进行信用额度检查。
销售设置 客户和销售单据的默认值 销售团队希望每份报价单都使用相同的区域和价格表行为。
采购设置 供应商和采购单据的行为 采购部门决定在开票前是否需要采购订单。
库存设置 仓库、估值、预留以及库存交易行为 仓库用户需要序列号和负库存规则与公司政策保持一致。
制造设置 生产默认值和制造控制 组装团队希望工单和工序卡遵循既定的产能流程。
项目设置 项目、任务和工时跟踪的默认值 维修服务根据与项目关联的工时表进行计费。
支持设置 问题和服务级别的行为 支持团队将客户提交的问题分配给默认服务团队。
CRM 设置 线索、商机和沟通行为 销售团队希望网站咨询能转化为归属一致的线索。

选择正确的设置页面

从需要更改的交易或流程入手。如果更改只应影响销售订单,请打开销售设置。如果影响所有用户,例如密码策略或日期格式,请使用系统设置。如果只是为新记录提供通用的公司或货币默认值,请使用全局默认值。如果只在单个用户会话期间生效,请使用会话默认值。

保存模块设置后,创建一个新的草稿交易并确认预期结果。已提交的现有交易通常会保留之前的状态,不应将其视为新配置失败的证据。

故障排除

模块设置对现有单据没有影响

重新加载表单并用新的草稿进行测试。某些设置可能仅在创建单据时或满足特定交易条件时才会生效。

找不到某个设置页面

确认应用和模块已安装、相关领域已启用,并且用户拥有系统管理员或其他所需角色。

用户仍然可以打开某个模块

模块设置控制的是行为,而不是访问权限。访问控制需要通过角色、角色权限、用户权限和工作区可见性来配置。

常见问题

模块设置适用于所有公司吗?

许多模块设置是全站范围的,而某些行或默认值是针对特定公司的。在推广之前,请阅读字段说明并用受影响的 company 进行测试。

模块设置能替代公司默认值吗?

公司默认值提供科目和其他公司级别的值。模块设置控制流程行为。两者可以因不同原因影响同一笔交易。

管理员应该同时更改多个设置吗?

每次只更改一个业务决策,并在新交易中验证。这样更容易追踪意外结果。

  • 系统设置
  • 全局默认值
  • 会话默认值
  • 领域设置
  • 基于角色的权限
  • 工作区

速率限制

Frappe 框架开箱即用地支持对 HTTP 请求进行速率限制。

Frappe 框架基于请求所消耗的时间实现固定窗口速率限制。该限制针对在配置窗口内所有 HTTP 请求所花费时间之和进行强制约束。每经过 window 秒后周期会重置,例如,将 window 设置为 3600 秒,将根据站点所在时区在每个小时开始时将使用计数器重置为 0。

注意:超过限制的请求不会被处理,并会返回 HTTP 429(请求过多)响应。

你可以通过在 site_config.json 中添加类似以下配置来为你的站点启用速率限制:

{
 "rate_limit": {
 "limit": 600,
 "window": 3600
 }
}
键 描述
limit 在速率限制窗口内允许使用的最大时间量(以秒为单位)。
window 速率限制窗口的大小(以秒为单位)。

每个 HTTP 请求返回的 HTTP 头都会显示当前的速率限制状态,例如

curl -i https://frappe.io/docs
HTTP/1.1 200 OK
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 518060453
X-RateLimit-Reset: 3513
X-RateLimit-Used: 100560

如果在配置的限制耗尽后仍有请求发出,则会返回 HTTP 429 响应以及速率限制状态:

curl -i https://frappe.io/docs
HTTP/1.1 429 TOO MANY REQUESTS
X-RateLimit-Limit: 600000000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1242
Retry-After: 1242
头 描述
Retry-After 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Limit 在一个速率限制窗口内允许使用的时间(以微秒为单位)。
X-RateLimit-Remaining 当前速率限制窗口中剩余(可使用)的时间(以微秒为单位)。
X-RateLimit-Reset 当前速率限制窗口重置前剩余的时间(以秒为单位)。
X-RateLimit-Used 用于处理当前请求所使用的时间(以微秒为单位)。

特定端点的速率限制

站点范围的配置限制的是请求的总处理时间,而开发者可以使用位于 frappe.rate_limiter 中的 @rate_limit 装饰器,对特定的白名单 Python 方法应用严格的请求次数限制。

装饰器参数:

参数 类型 默认值 描述
key str None 来自 frappe.form_dict 的特定键,用于唯一标识该请求。
limit int 或 Callable 5 在时间窗口内允许的最大请求数。
seconds int 86400 时间窗口,以秒为单位(默认是 24 小时)。
methods str 或 list "ALL" 要进行速率限制的 HTTP 方法(例如 "POST" 或 ["GET", "POST"])。
ip_based bool True 按请求者的 IP 地址进行限制跟踪(网络感知)。
endpoint str None (在 v17 中添加)自定义计数器名称。如果可调用对象没有稳定的点分路径(例如 functools.partial),则此项为必填。
user_based bool False (在 v17 中添加)按已认证用户的会话 ID 进行限制跟踪(身份感知)。

跟踪行为与互斥性 (身份感知跟踪和安全访客回退从版本 17 开始可用。在 v15 或 v16 中使用 user_based 或 endpoint 会引发 TypeError)。

速率限制器旨在按网络身份或用户身份来跟踪请求,而不会同时按两者跟踪。

  • 身份感知(user_based=True):限制严格应用于已认证的 frappe.session.user。请求者的 IP 地址会被完全忽略,从而允许共享企业网络(NAT)中的用户避免无意中互相阻塞。
  • 网络感知(ip_based=True, user_based=False):限制严格应用于 request_ip。用户的会话 ID 会被忽略。这是默认的旧版行为。
  • 访客回退:如果某个端点配置了 user_based=True,未认证的“访客”流量将安全地回退为按其 IP 地址进行跟踪。这可以防止单个恶意访客耗尽全局访客配额,从而导致所有其他匿名用户遭遇拒绝服务。

示例实现:

from frappe.rate_limiter import rate_limit

# Limits authenticated users to 10 requests per minute per account.
# Guests hitting this mixed-auth endpoint are limited to 10 requests per minute per IP.
@frappe.whitelist()
@rate_limit(limit=10, seconds=60, user_based=True)
def generate_heavy_report():
    pass

扩展自动化

应用可以通过三种方式扩展自动化流程:发出事件,让流程据此触发或等待;添加操作,加入步骤列表;以及添加关系,让步骤能够访问架构未直接关联的记录。每一种都在应用的 hooks.py 中注册。

发出事件

在应用中发生有意义的事情时,调用 emit():

from frappe.automation_engine import emit

def on_deal_won(deal):
    emit("crm.deal_won", doc=deal, payload={"amount": deal.deal_value})
参数 说明
event 事件名称。使用带应用前缀的点分名称,例如 crm.deal_won。
doc 事件所涉及的单据。带有文档类型的自定义事件流程会针对它运行。
payload 一个可 JSON 序列化的字典。流程在模板中通过 payload 读取它,在条件中通过 context["payload"] 读取它。
correlation_key 恢复暂停在匹配的 WaitForEvent 步骤上的运行。

emit() 做两件事:

  1. 它为该事件排队所有已启用的自定义事件流程。
  2. 如果提供了 correlation_key,它会恢复任何使用相同键等待该事件的运行。

它返回 {"queued": <count>, "resumed": <count>}</count></count>。运行会在当前事务提交后开始,在此之前对同一记录调用两次 emit() 只会产生一次运行。超过自动化设置中事件负载限制(默认 64 KB)的负载会被拒绝。

恢复等待中的流程

流程可以先发送一封邮件,然后用关联键 {{ doc.name }} 执行 WaitForEvent crm.email_replied。当回复到达时,你的应用用相同的键发出相同的事件:

emit(
    "crm.email_replied",
    doc=lead,
    payload={"communication": comm.name},
    correlation_key=lead.name,
)

等待中的运行会立即恢复,其后续步骤可以从 context["event"]["payload"] 读取负载。

注册事件

emit() 只接受已安装应用注册过的事件,除非在自动化设置中开启了允许未注册事件。在 hooks.py 中注册事件,可以是裸名称,也可以带有构建器界面可以显示的元数据:

automation_events = [
    "crm.deal_won",
    {
        "crm.email_replied": {
            "label": "Email replied",
            "description": "A contact replied to an email sent from CRM",
            "subject": {"doctype_key": "reference_doctype", "name_key": "reference_name", "doctypes": ["CRM Lead", "CRM Deal"]},
        }
    },
]

subject 告诉引擎事件涉及哪条记录,从负载中读取。有了主体,CRM Lead 上的流程会针对负载中指定的线索运行,即使发出事件的代码当时持有的是 Communication。当主体类型固定时,用 "doctype": "CRM Lead" 代替 doctype_key。

添加操作

继承 AutomationAction 并实现 execute:

# my_app/automation.py
from typing import ClassVar

import frappe
from frappe.automation_engine.actions.base import AutomationAction, AutomationParamError, render_value

class PostToChannel(AutomationAction):
    action_type = "PostToChannel"
    label = "Post to Channel"
    description = "Post a message to a team channel."
    transactional = False  # it calls an external service
    params_schema: ClassVar[list] = [
        {"fieldname": "channel", "label": "Channel", "fieldtype": "Data", "reqd": 1},
        {"fieldname": "message", "label": "Message", "fieldtype": "Small Text", "reqd": 1},
    ]

    def validate(self, params, doctype):
        if not params.get("channel"):
            raise AutomationParamError("Channel is required", fieldname="channel")

    def execute(self, doc, params, context):
        message = render_value(params["message"], doc, context)
        post_message(params["channel"], message)
        return f"Posted to {params['channel']}"
# hooks.py
automation_actions = ["my_app.automation.PostToChannel"]

类属性:

属性 用途
action_type 存储在步骤上的唯一标识符。发布后不要更改。
label、description 显示在操作类型列表和构建器界面中。
params_schema 参数字段:fieldname、label、fieldtype、options、reqd。
requires_document 如果操作可以在没有触发单据的情况下运行(例如在计划流程上),则设为 False。
applicable_doctypes 将操作限制为这些 DocType。None 表示全部。
supported_trigger_types 将操作限制为这些触发类型。None 表示全部。
transactional 如果操作具有数据库回滚无法撤销的副作用(例如 HTTP 调用或消息),则设为 False。这样引擎就永远不会从头重放该运行。

方法:

  • validate(params, doctype):在流程保存时运行,并在每次执行前再次运行。抛出带有 fieldname 的 AutomationParamError 来指向错误的参数。
  • execute(doc, params, context):执行实际工作。返回一个短字符串用于运行日志,或返回一个包含 detail 键的字典,以及后续步骤应通过 context["steps"][step_key] 读取的任何输出。
  • output_doctype(params):如果操作创建了一条记录,返回其 DocType。这样步骤的输出别名就可以指向新记录,后续步骤也会针对它进行验证。

操作以流程的运行身份用户运行,因此正常的权限检查会生效。不要调用 frappe.db.commit();事务由引擎管理。要从操作内部暂停运行,请抛出 StopAutomation("reason", resume_after=seconds)。

添加关系

Link、Dynamic Link 和子表链接字段会自动成为双向关系。只有当架构无法表达某种连接时(例如通过电子邮件地址匹配的记录),才需要注册提供程序:

from frappe.automation_engine.relationships import AutomationRelationshipProvider

class ContactRelationships(AutomationRelationshipProvider):
    def get_definitions(self, source_doctype):
        if source_doctype != "Communication":
            return []
        return [{
            "name": "sender_lead",
            "label": "Lead (by sender email)",
            "target_doctype": "CRM Lead",
            "cardinality": "one",
        }]

    def resolve(self, source_doc, relationship, params):
        name = frappe.db.get_value("CRM Lead", {"email": source_doc.sender})
        return [{"doctype": "CRM Lead", "name": name}] if name else []
# hooks.py
automation_relationships = ["my_app.automation.ContactRelationships"]
automation_relationship_ignore = ["My Log DocType"]
  • cardinality 是 one 或 many。只有 one 关系可以用作步骤的目标别名。many 关系用于相关记录条件,此时覆盖 query() 以在 SQL 中过滤可以让它们在大表上保持高效。
  • 与派生关系具有相同 name 的定义会替换它,这允许应用重新标记架构链接。
  • 提供程序返回的每个引用在步骤使用之前都会检查其存在性和读取权限。
  • automation_relationship_ignore 会从派生关系中隐藏 DocTypes(例如日志)。

抑制和检查自动化

from frappe.automation_engine import is_enabled, skip_automations

if is_enabled():
    ...

with skip_automations():
    doc.save()  # no Automation Flow fires for this save

步骤与操作

自动化流程中的操作表按从上到下的顺序列出了每次运行所执行的步骤。每一行都有一个步骤类型,对于操作步骤,还有一个操作类型及其参数。

步骤类型

步骤类型 作用
操作 执行一个操作,例如设置字段或发送邮件
等待 将运行暂停一段固定时间,然后继续执行下一步
等待事件 将运行暂停,直到某个应用发出匹配的事件,或超时
条件判断 评估一个条件,只运行匹配分支中的步骤

等待

将参数设置为一个时长:

{"value": 2, "unit": "Days"}

unit 是 Seconds、Minutes、Hours 或 Days 之一。暂停期间,运行显示为等待中。恢复时,文档会重新加载,因此后续步骤能看到等待期间所做的任何更改。

等待事件

暂停直到某个应用发出带有匹配关联键的事件:

{
  "event_name": "crm.email_replied",
  "correlation_key": "{{ doc.name }}",
  "timeout_value": 3,
  "timeout_unit": "Days"
}

关联键在到达该步骤时渲染,当应用以相同的事件和键调用 emit() 时,运行会恢复。如果超时先到,运行也会恢复。等待之后的步骤可以从 context.event 读取它是如何结束的:outcome 是 Matched 或 Timed Out,而 payload 保存事件的数据。

一个典型用例:只有在客户未回复时才发送跟进,方法是在等待之后添加一个条件判断步骤,条件为 context["event"]["outcome"] == "Timed Out"。

条件判断

条件判断步骤需要一个步骤条件。属于它的步骤要将父步骤设置为该条件判断行的编号,并将分支设置为 If(条件为真时运行)或 Else(条件为假时运行)。没有父步骤的步骤始终运行。

# 步骤类型 操作 父步骤 分支
1 条件判断 条件 doc.grand_total > 10000
2 操作 分配给用户:manager@example.com 1 条件为真
3 操作 分配给用户:sales@example.com 1 条件为假
4 操作 发送通知

条件判断步骤可以嵌套。分支在运行首次到达条件判断步骤时决定一次,之后在等待之后不会重新评估。

步骤条件

任何步骤都可以有一个步骤条件,这是一个 Python 表达式,必须为真该步骤才会运行。否则该步骤会被记录为已跳过,运行继续。该表达式可以读取:

  • doc:触发运行的文档
  • target:该步骤所操作的记录
  • context:运行的状态,包括 context["steps"],即按步骤键索引的先前步骤的输出

操作

这些是作为核心框架操作提供的操作。其他应用可以通过 hooks.py 扩展这些操作,参见扩展自动化。

操作类型 作用 关键参数
设置字段值 在目标上设置一个或多个字段并保存 field + value,或作为映射的 values
递增字段值 向数值字段加一个数 field、amount(负数表示减去)
创建文档 插入任意类型的新文档 doctype、values
发送通知 发送邮件或系统通知 channel(Email 或 System)、recipients、subject 和 message,或 email_template
分配给用户 将目标分配给一个或多个用户 assign_to、description
调用 Webhook 发送 HTTP 请求 url、method、headers、payload、timeout
运行脚本 运行 Python 代码 server_script(一个 API 服务器脚本),或内联 script

示例

一次设置两个字段:

{"values": {"status": "Replied", "priority": "High"}}

创建一个跟进待办事项:

{
  "doctype": "ToDo",
  "values": {
    "description": "Call {{ doc.lead_name }}",
    "reference_type": "{{ doc.doctype }}",
    "reference_name": "{{ doc.name }}",
    "allocated_to": "{{ doc.lead_owner }}"
  }
}

给文档所有者和被分配人发送邮件:

{
  "channel": "Email",
  "recipients": ["@owner", "@assignees", "team@example.com"],
  "subject": "{{ doc.name }} was closed",
  "message": "
<p>{{ doc.name }} was closed on {{ frappe.utils.nowdate() }}.</p>"
}

@owner 和 @assignees 在步骤运行时针对文档解析。它们读取的是运行开始时(或最后一次等待之后)加载的文档,因此同一次运行中先前步骤所做的分配还不会包含在内。

模板

文本参数接受 Jinja。可用变量:

  • doc / target:该步骤所操作的记录
  • trigger:启动本次运行的文档
  • payload:来自自定义事件或计划任务的数据
  • context:本次运行的状态,包括用于存放前面步骤输出的 context.steps.<step_key></step_key>

对其他记录执行操作

默认情况下,步骤作用于触发文档。若要对关联记录执行操作,请在 Relationships 下声明一个命名别名,并将步骤的 Target 设置为该别名:

[{"alias": "customer", "relationship": "customer"}]

Link、Dynamic Link 和子表链接字段会自动以其字段名提供。反向链接命名为 <doctype>_via_<field></field></doctype>,例如 crm_deal_via_lead。应用可以注册更多(参见 Extending Automations)。

Create Document 步骤还可以设置 Output Alias。之后的步骤便可以以它创建的文档为目标。

错误处理

  • Stop on Error(默认开启):第一个失败的步骤会停止本次运行,运行被标记为 Failed。关闭它可继续执行;此时运行被标记为 Partially Failed。
  • 每个步骤都在自己的保存点内运行,因此失败步骤的数据库更改会被回滚,而不会撤销之前的步骤。
  • Call Webhook 和 Run Script 可以访问数据库外部。一旦其中之一运行过,本次运行就绝不会从头重放,因此 webhook 不会被发送两次。
  • Call Webhook 会拒绝私有和内部网络地址,并且最多跟随五次重定向。响应状态为 400 或以上会导致该步骤失败。
  • Run Script 需要启用 Server Scripts(bench set-config -g server_script_enabled 1),并且只有 System Manager 才能编写内联脚本。脚本可以读取 doc、target、trigger、payload 和 context,并可以在 result 上设置键,以将输出发布给后续步骤。它不能调用 frappe.db.commit()。

测试运行

在已保存的流程上点击 Test Run,然后选择一个文档。流程会针对它真实运行,随后 所有更改都会被回滚。结果会列出每个步骤的状态、消息以及它读取到的条件值。

在测试运行期间,Wait 步骤会被模拟,Call Webhook 会报告它将会发送的请求,但不会实际发送。

触发器和条件

触发器决定自动化流程何时被考虑。匹配字段和条件决定某个特定文档是否符合条件。两者都在自动化流程表单上设置。

触发器类型

触发器类型 触发时机 所需配置
文档创建 插入文档时 文档类型
文档更新 保存已有文档时(插入时不触发) 文档类型
字段值变更 保存时某个字段的值发生变化 文档类型、触发字段
文档删除 删除文档时 文档类型
文档提交 提交可提交的文档时 文档类型
文档取消 取消可提交的文档时 文档类型
基于日期 日期字段在今天的 N 天前或 N 天后 文档类型、日期字段
定时调度 cron 表达式到期时 Cron 表达式
自定义事件 应用调用 frappe.automation_engine.emit() 时 自定义事件名称
手动 有人显式启动流程时 —

字段值变更

将触发字段设置为要监视的字段。可选地通过以下方式缩小范围:

  • 原值:仅当旧值为该值时触发。
  • 新值:仅当新值为该值时触发。

任一项留空表示接受任意值。例如,触发字段为 status,新值为 Closed,则每当文档被关闭时触发,无论其之前的状态是什么。

基于日期

选择一个日期字段、一个日期偏移(天)和一个日期方向:

  • 3 天之前 renewal_date:对续约日期距今三天的文档触发。
  • 1 天之后 due_date:对昨天变为逾期的文档触发。

基于日期的流程每小时检查一次。无论检查运行多少次,每个文档每天最多触发一次。

定时调度

输入标准的五字段Cron 表达式,例如 0 9 * * 1 表示每周一 09:00。保存后表单会显示下次运行时间。

  • 不设置文档类型时,流程按调度运行一次,不涉及文档。适用于周期性任务,如每周摘要邮件或调用外部 API。
  • 设置文档类型时,每次调度到期时,流程会为每个匹配其匹配字段和条件的文档各运行一次。

如果调度器宕机错过了多次触发,流程只会为最近一次补运行一次,而不是为每次错过的触发都补运行。

自定义事件

自定义事件流程在应用发出命名事件时运行,例如 crm.deal_won。在自定义事件中按应用发出的事件名称准确输入。应用如何发出和注册事件在扩展自动化中介绍。

如果设置了文档类型,流程仅对该类型文档相关的事件运行。

手动

手动流程不会自行触发。仅当应用通过 frappe.automation_engine.api.run_manually 方法启动它时才运行,例如从应用 UI 中的按钮启动。调用者需要对流程有写权限,对文档有读权限。

匹配字段

匹配字段使用与列表视图相同的筛选编辑器。每个字段规则添加一行。文档必须满足所有规则,流程才会运行。

例如,在一个线索流程中:

  • 状态 = Open
  • 来源已设置
  • 年收入 > 1000000

没有匹配字段的流程会匹配其类型的所有文档。

高级条件

对于匹配字段无法表达的逻辑,在使用 Python 的高级表达式中编写 Python 表达式。文档可通过 doc 访问,表达式使用 Frappe 的安全求值器求值,因此可以使用 frappe.utils 辅助函数。

doc.status == "Open" or doc.priority == "High"
doc.grand_total > 50000 and doc.customer_group != "Internal"

匹配字段和表达式必须同时为真。如果表达式抛出错误,文档将被视为不匹配,错误会写入错误日志。

运行时重新验证

触发器在文档保存时匹配,但步骤稍后在后台工作进程中运行。勾选运行时重新验证可在执行前再次测试匹配字段和条件。如果文档不再匹配,该次运行将记录为已跳过。

当文档在触发流程的保存之后可能快速变化时(例如状态被设置后又还原),这很有用。

运行身份

运行身份决定步骤以谁的权限运行:

  • 自动化用户(默认):自动化用户字段中的用户,除非你更改它,否则为 Administrator。
  • 触发用户:其操作触发了流程的用户。
  • 文档所有者:流程所针对的文档的所有者。

每个步骤仍以该用户的身份运行正常的权限检查。如果某个步骤因权限错误而失败,请检查“运行身份”用户是否能够读取触发文档并写入该步骤所更改的记录。

当流程未触发时

在安装、迁移和补丁期间,当自动化设置中勾选了禁用自动化时,以及在 site_config.json 中设置了 automation_disabled 时,自动化流程会被跳过。

必须在不触发流程的情况下保存文档的代码(例如数据导入)可以抑制流程:

from frappe.automation_engine import skip_automations

with skip_automations():
    doc.save()

自动化

自动化流程的意思是“当这类记录发生这件事时,按顺序执行这些操作”,无需编写代码。你选择一个触发器,用条件缩小范围,然后添加一系列步骤:更新字段、发送通知、分配人员、调用 webhook、等待两天、根据某个值分支。

何时使用自动化流程

Frappe 已经为常见任务提供了单一用途的工具,当其中某个工具正好满足你的需求时,它仍然是正确的选择:

你想要… 使用
在某个事件发生时发送一封邮件或系统通知 通知
在某个事件发生时将文档提交到外部 URL Webhook
在团队中分配新文档 分配规则
用代码更改文档的行为 服务器脚本

当你需要这些工具单独无法完成的事情时,请使用自动化流程:

  • 按顺序执行多个步骤:分配文档,然后给被分配人发邮件,然后创建一个后续任务。
  • 等待:做某件事,等待两天,然后再做另一件事,或者等到另一个事件发生。
  • 分支:如果交易金额超过 10,000,就做一件事,否则做另一件事。
  • 访问关联记录:触发器是一条 Communication,但你想更新的记录是它所属的 Lead。

工作原理

  1. 触发器:文档事件、日期、计划、来自应用的自定义事件或手动运行会启动流程。请参阅触发器和条件。
  2. 匹配:流程的匹配字段和条件决定此文档是否符合条件。
  3. 排队:匹配的触发器会被排队,而不是内联执行。后台工作进程会在请求提交后不久处理它,因此缓慢的 webhook 永远不会阻塞用户的保存操作。在工作进程运行之前,对同一文档的重复保存会合并为一次运行。
  4. 运行:工作进程按顺序执行步骤,并写入一条自动化运行记录,其中包含每个步骤的结果。请参阅步骤和操作。

由于运行发生在后台,因此调度器和后台工作进程必须正在运行,自动化才会触发。

创建流程

  1. 在 awesome bar 中转到自动化流程,然后点击新建。
  2. 输入标题,并选择流程所涉及的文档类型。
  3. 选择触发器类型,并填写它要求的字段。
  4. 可选地添加匹配字段和使用 Python 的高级表达式。
  5. 在操作下添加一行或多行。
  6. 保存并点击测试运行,针对真实文档进行尝试(所有更改都会回滚),然后勾选已启用并再次保存。

流程只有在至少有一个操作时才能启用。

示例:跟进高价值线索

  • 文档类型:Lead
  • 触发器类型:文档创建
  • 匹配字段:年收入 > 1,000,000
  • 操作:
    1. 分配给用户:sales-head@example.com
    2. 发送通知:电子邮件发送至 sales-head@example.com,主题为 New lead: {{ doc.lead_name }}
    3. 等待:2 天
    4. 发送通知:发送邮件至 @assignees,主题为 Follow up on {{ doc.lead_name }},并附带一个步骤条件 doc.status == "Lead",这样如果已经有人推进了该线索,就会跳过此步骤。

运行历史

每次运行都会保存为一条自动化运行记录,该记录会关联到对应的流程以及它所运行的文档。每次运行都有一个状态:

  • 成功:每个步骤都成功执行或被跳过。
  • 部分失败:某个步骤失败,且出错时停止处于关闭状态,因此后续步骤仍会继续运行。
  • 失败:某个步骤失败,且 Stop on Error 停止了运行,或者运行无法启动。
  • 等待中:运行在 Wait step 处暂停,并会自动恢复。
  • 已跳过:当 Revalidate on Run 开启时,文档已被删除或不再匹配。

打开一次运行,可以查看每个步骤的状态、输出、耗时,对于失败的步骤,还可以查看错误信息和堆栈跟踪。对于被跳过的步骤,运行记录会显示条件及其读取到的值,这样你就能看出它为什么没有匹配。运行日志会按照标准日志清理机制在 30 天后清除。

自动化设置

自动化设置包含站点范围的开关和限制:

  • 禁用自动化:停止在站点上派发所有流程。已排队的记录不受影响。若要在数据库不可达时也停止自动化,请在 site_config.json 中设置 "automation_disabled": 1。
  • 最大递归深度(默认 3):一个流程的操作可以触发另一个流程多少次,超过后该链条将被拒绝。这可以防止两个相互更新的流程无限循环。
  • 失败阈值(默认 10):在连续这么多次运行失败后,该流程会被自动禁用,其待处理的运行会被跳过,并且其所有者会收到通知。原因会显示在该流程的禁用原因字段中。
  • 允许未注册事件:接受没有任何已安装应用注册的自定义事件。请参阅扩展自动化。

其余设置(排空时机、重试、保留期限和大小限制)用于调整后台工作进程,通常很少需要更改。

后续步骤

  • 触发器与条件
  • 步骤与操作
  • 扩展自动化(面向应用开发者)

数据库优化 – 硬件与配置

在使用 Frappe 框架时,优化性能的一个重要部分就是优化数据库的性能。幸运的是,MariaDB/MySQL 的性能是一个被广泛理解的概念,有大量资源可以深入学习。Frappe 与许多基于相同数据库构建的其他 Web 应用并无不同,所以如果你知道如何为 MySQL 做优化,那你已经知道在使用 Frappe 框架时该怎么做了。

架构与硬件

我们建议将数据库服务器与应用服务器分开部署。以下是这样建议的一些原因:

  • 应用服务器经常会出现负载高峰和动态的内存使用模式。将它和数据库服务器放在一起会影响整体性能。
  • 数据库需要持续稳定的硬件资源才能表现良好,包括尽可能多的 RAM 和 IOPS。
  • 数据库和应用服务器通常受益于不同类型的硬件调优和配置。

CPU

我们建议至少从 2 个 vCPU 起步。vCPU 的数量决定了你的数据库服务器能执行多少”真正并行”的工作。你应该在数据库服务器上设置一些监控(参见下面的监控部分)。当你的利用率持续达到 60%,或长时间保持在 50% 以上时,你可能需要升级 vCPU 数量,以提供更多并行计算资源。

内存

从磁盘访问数据比从内存访问慢好几个数量级。数据库使用 RAM 作为”缓冲池”,充当磁盘上实际数据的缓存。一个调优良好、配置充足的数据库在超过 99% 的情况下都不会因为读取数据而访问磁盘。随着数据库增长,当你需要大量数据能够被非常快速地访问时,你可能需要考虑增加更多 RAM,并重新配置服务器以利用可用的 RAM。

我们建议至少从 8GB RAM 起步。

> 注意:建议数据库服务器不要依赖 SWAP。数据库本身已经实现了在有限内存下工作所需的所有逻辑,添加 swap 只是重复了这项工作。你应该保留少量 SWAP 内存作为安全保障,但将 swappiness 降到尽可能低的值。

磁盘与 IOPS

不要在磁盘存储和 IOPS 上省钱。考虑购买至少是实际使用量 3 倍的存储。你的数据库会随着时间自然增长,你不会想每次磁盘用完就升级。例如 AWS,额外的存储也会给你更多 IOPS。我们建议从大约 2000-3000 IOPS 起步,并监控使用情况,以判断是否需要更多还是可以减少。

至少从 50GB 磁盘起步,并考虑大约是你当前数据库大小的 3 倍。例如,如果你的数据库是 20 GB,那么考虑购买 60GB 存储的服务器或可挂载卷。

扩展资源

我们建议尽可能长时间地进行垂直扩展,大致来说就是垂直扩展到至少 64vCPU/128GB RAM。你可以利用监控设置中的洞察来决定需要升级哪个组件。请记住,大多数云服务商会将资源捆绑成”实例”,所以只升级系统的某一部分并不那么简单。

如果你认为已经达到了单台机器所能达到的极限,那么可以考虑设置数据库副本。Frappe 框架可以配置为将所有繁重的读取操作路由到数据库副本,以减轻主服务器的负载。

配置

数据库开箱即提供了数百个可调参数,用于调整数据库性能的不同方面。至少要把最重要的参数调好,这样才能最大化你的硬件能力。

缓冲池 – innodb-buffer-pool-size = 5G

你需要调整的最重要的参数是缓冲池大小。由于历史原因,它的默认配置也是最差的,所以如果不去配置它,你根本没有利用好你的硬件。

如前所述,缓冲池充当磁盘上数据的缓存。缓冲池大小的经验法则是:

“缓冲池大小应该能容纳你的工作数据集”

说起来简单,但很难量化。你的数据”工作集”是你每天都会访问的数据,是你希望非常快速访问、不能接受随机变慢的数据。让我们以 ERP 为例,来理解如何选择比较理想的缓冲池大小,进而确定 RAM 大小。

  1. 你的站点有 80GB 的数据库。
  2. 这个数据库是 4 年间积累生成的。
  3. 你认为去年数据库增长了 20GB,而在 ERP 中,你主要访问的是 1 年的数据,用于各种报表和验证。
  4. 所以起步时理想的缓冲池大小是 20GB。
  5. 理想情况下,缓冲池应占用约 60% 的 RAM。所以你应该选择 20 / 0.6 ≈ 33GB RAM。

你可以对缓冲池使用情况设置监控,以了解是否需要更多或更少。两个最重要的指标:

  1. 缓冲池命中率:如果超过 99%,那么很可能你不需要增加更多缓冲池大小。
  2. 利用率:如果你的利用率没有接近 95%-100%,那你肯定不需要增加更多缓冲池。

缓冲池缓存会随时间填满,因此你需要在运行几天后进行测量。你可以输入 SHOW ENGINE INNODB STATUS 来查看这些统计数据。这会输出大量内容,但以下是重要部分:

----------------------
BUFFER POOL AND MEMORY
----------------------
Buffer pool size   2790528
Free buffers       1594
Buffer pool hit rate 999 / 1000

其他参数

以下是我们在 Frappe 使用的配置。你可以从这开始,并调整某些参数。最重要的参数将在下面更详细地解释。

  • innodb-flush-method = O_DIRECT:绕过某些操作并执行“直接 IO”。
  • innodb-flush-log-at-trx-commit = 1:通过在每次提交时写入日志来提高系统的持久性。不要为了小幅性能提升而牺牲这种持久性。
  • innodb-file-per-table = 1:为每个表创建一个文件。
  • slow-query-log = 1 和 slow-query-log-file = /var/lib/mysql/mysql-slow.log:将运行时间过长的任何查询存储在日志文件中。这对于事后找出哪些查询应优化很有用。
  • query-cache-type = 0 和 query-cache-size = 0:这会禁用查询缓存,尽管名字如此,但现在普遍认为它对性能有害。
[mysqld]
# GENERAL #
default-storage-engine          = InnoDB

# MyISAM #
key-buffer-size                 = 32M
myisam-recover                  = FORCE,BACKUP

# SAFETY #
max-allowed-packet              = 256M
max-connect-errors              = 1000000
innodb                          = FORCE

# DATA STORAGE #
datadir                         = /var/lib/mysql/

# BINARY LOGGING #
log-bin                         = /var/lib/mysql/mysql-bin
log_bin_index                   = /var/lib/mysql/mysql-bin.index
expire-logs-days                = 14
sync-binlog                     = 1

# CACHES AND LIMITS #
tmp-table-size                  = 32M
max-heap-table-size             = 32M
query-cache-type                = 0
query-cache-size                = 0
max-connections                 = 500
thread-cache-size               = 50
open-files-limit                = 65535
table-definition-cache          = 4096
table-open-cache                = 10240
tmp-disk-table-size             = 5120M
max-statement-time              = 10800

# INNODB #
innodb-flush-method             = O_DIRECT
innodb-log-files-in-group       = 2
innodb-log-file-size            = 512M
innodb-flush-log-at-trx-commit  = 1
innodb-file-per-table           = 1
# NOTE: This has to be configured based on RAM. Consider ~60% of available RAM. This example assumes 8GB RAM.
innodb-buffer-pool-size         = 4.8G
innodb-file-format              = barracuda
innodb-large-prefix             = 1
innodb-old-blocks-time          = 5000
collation-server                = utf8mb4_unicode_ci
character-set-server            = utf8mb4
character-set-client-handshake  = FALSE
max_allowed_packet              = 512M

# LOGGING #
log-error                       = /var/lib/mysql/mysql-error.log
log-queries-not-using-indexes   = 0
slow-query-log                  = 1
slow-query-log-file             = /var/lib/mysql/mysql-slow.log

[mysql]
default-character-set           = utf8mb4

[mysqldump]
max_allowed_packet              = 512M

监控

Frappe 没有针对 MariaDB 的特定监控工具,我们建议设置可观测性工具,如 Prometheus 配合 Grafana 来制作仪表板。

  • 内置慢查询日志:查看慢查询被记录的频率,并使用 Percona toolkit 中的 pt-query-digest 等工具找出最常见的慢查询。

  • 使用以下导出器设置 Prometheus 和 Grafana:

    • Node Exporter – 提供整体系统统计信息。对于监控 CPU、RAM 和 I/O 很有用。
    • MySQLD Exporter – 从 MariaDB 的数据库引擎提供大量有用的指标。
    • PMM – Percona 的工具包,带有预构建的仪表板。

我们认为以下指标值得跟踪:

  1. CPU 使用率 – 持续接近 100% 使用率意味着你可能需要更多 CPU 能力来处理负载。
  2. RAM – 配置良好的数据库的 RAM 使用率会徘徊在 90% 左右。使用率接近 100% 意味着你可能会交换或导致系统崩溃。使用率显著低于 90% 意味着你没有将所有可用 RAM 用于缓冲池缓存。
  3. 磁盘 I/O – 一旦数据库服务器充分预热,磁盘读取应该最少。所有读取负载都将从缓冲池提供,只有更新需要写入磁盘。如果你看到大量磁盘 I/O,这可能意味着缓冲池大小不足或查询需要将临时文件写入磁盘。
  4. “InnoDB 行读取” – 读取次数突然增加可能意味着流量增加或引入了未优化的查询。
  5. “BP 未命中率” – 大部分情况下应小于 1%。如果很高,那么你的缓冲池大小可能不够。
  6. “Innodb 行锁等待负载” – 高锁等待负载意味着你有争用问题,多个工作进程试图写入相同资源但被阻塞。这个问题的典型解决方案是将工作排队而不是并行执行。也可能有未优化的查询导致锁被长时间持有。
  7. “InnoDB 缓冲池 LRU 子链变动” – 缓冲池将数据库页面列表维护为近似 LRU。逻辑备份等活动可能会将磁盘上的整个数据库读入缓冲池,导致最常用的页面被驱逐。理想情况下,在非工作时间进行备份以避免这种情况。

事件流

事件流是 Frappe 的一项功能,用于从一个 Frappe 站点发布文档变更,并在另一个站点上消费这些变更。当需要在站点之间同步选定的 DocType,而又不想为每种记录类型单独构建集成时,这个功能非常有用。

事件流的工作原理

在典型的配置中,一个站点充当事件生产者,另一个站点充当事件消费者。当选定的文档在生产者站点上被创建或更新时,事件会被记录下来,然后由另一个站点消费。这样就可以在多个 Frappe 站点之间复制主数据、交易数据或自定义 DocType。

版本 15 及之后的状态

从 Frappe/ERPNext 版本 15 开始,事件流不再作为受维护的核心功能提供支持。在 Frappe 版本 14 中,事件流存在于框架源码的 frappe/event_streaming 下。在版本 15 中,Frappe 添加了一个移除补丁,用于移除事件流模块及其 DocType,包括事件生产者、事件消费者、事件更新日志以及相关的映射 DocType。

独立应用

该功能被拆分成了一个独立的应用:frappe/event_streaming。然而,这个应用并没有得到积极维护。该 GitHub 仓库已于 2024 年 12 月 6 日被 Frappe 归档,目前为只读状态,且没有发布任何版本。除非你的团队准备好独立测试和维护它,否则在新的生产环境中应将其视为不受支持的功能。

  • REST API:使用 Frappe 的 REST API 在 ERPNext 和其他系统之间进行明确的集成。
  • Webhook:当文档创建或更新后需要通知另一个系统时,使用 Webhook。
  • 后台任务:对于需要重试、日志和错误处理的同步逻辑,使用定时任务或队列任务。
  • 自定义集成应用:当需要仔细控制数据映射、冲突处理或业务规则时,构建自定义应用。

需要注意的情况

避免在版本 15 或更高版本上围绕事件流规划新的实现。对于当前的 ERPNext 集成,建议优先使用 API、Webhook 和针对集成场景的队列,这样可以明确责任归属、重试行为和故障处理方式。

  • GitHub 上的 frappe/event_streaming
  • Frappe 版本 15 事件流移除补丁
  • Frappe REST API
  • Webhook
  • 后台任务