钩子

钩子(Hooks)允许你“挂钩”到 Frappe 框架核心部分的功能和事件中。此页面记录了框架提供的所有钩子。

跳转到 Frappe 中所有可用钩子的列表。

钩子是如何工作的?

钩子是核心代码中的一些位置,允许应用覆盖标准实现或对其进行扩展。钩子定义在你应用的 hooks.py 文件中。

让我们通过示例来学习。在你应用的 hooks.py 文件中添加以下钩子。

test_string = "value"
test_list = ["value"]
test_dict = {
    "key": "value"
}

现在,通过运行命令 bench --site sitename console 打开 Python 控制台,并运行以下代码行:

❯ bench --site sitename console
Apps in this namespace:
frappe, frappe_docs

In [1]: frappe.get_hooks("test_string")
Out[1]: ["value"]

In [2]: frappe.get_hooks("test_dict")
Out[2]: {"key": ["value"]}

In [3]: frappe.get_hooks("test_list")
Out[3]: ["value"]

当你调用 frappe.get_hooks 时,它会将列表中的所有值进行转换。这意味着如果钩子在多个应用中定义,则会从这些应用中收集值。这就是实现钩子级联特性的方式。

现在,钩子值可以通过不同方式使用。例如,使用 app_include_js 引入 JS 资源时,会包含所有值。但对于覆盖白名单方法,则使用列表中的最后一个值。

因此,钩子的实现完全取决于功能作者打算如何使用它。

如何解决钩子冲突?

钩子使用“最后写入者获胜”策略进行解析。站点上最后安装的应用将拥有最高优先级。

  • 当钩子覆盖现有行为(如覆盖类)时,只有来自最后安装的应用的覆盖才会生效。
  • 当钩子扩展行为时,扩展将按照在站点上的安装顺序应用。

如果你需要更改此顺序,可以前往“已安装应用”页面,并点击“更新钩子解析顺序”。

应用元数据

这些是在你创建新应用时自动生成的。大多数情况下,你无需更改此处任何内容。

  1. app_name – 应用的 slug 化名称
  2. app_title – 可展示的应用名称
  3. app_publisher
  4. app_description
  5. app_version
  6. app_icon
  7. app_color

JavaScript / CSS 资源

以下钩子允许你在站点的各个部分注入静态 JS 和 CSS 资源。

后台工作台

这些钩子允许你在渲染后台工作台的 desk.html 中注入 JS / CSS。

# injected in desk.html
app_include_js = "assets/js/app.min.js"
app_include_css = "assets/js/app.min.css"

# All of the above support a list of paths too
app_include_js = ["assets/js/app1.min.js", "assets/js/app2.min.js"]

门户网站

这些钩子允许你在渲染门户网站的 web.html 中注入 JS / CSS。

# injected in the web.html
web_include_js = "assets/js/app-web.min.js"
web_include_css = "assets/js/app-web.min.css"
# All of the above support a list of paths too
web_include_js = ["assets/js/web1.min.js", "assets/js/web2.min.js"]

网页表单

这些钩子允许你在用于渲染网页表单的 web_form.html 中添加静态 JS 和 CSS 资源。这些仅适用于标准网页表单。

webform_include_js = {"ToDo": "public/js/custom_todo.js"}
webform_include_css = {"ToDo": "public/css/custom_todo.css"}

对于用户创建的网页表单,你可以直接在表单本身中编写脚本。

页面

这些钩子允许你在标准后台页面中注入 JS 资源。

page_js = {"page_name" : "public/js/file.js"}

例如,后台作业是 Frappe 框架核心模块中的一个标准页面。要在该页面中添加自定义行为,你可以在自定义应用中添加一个 JS 文件 custom_app/public/js/custom_background_jobs.js,并在你的钩子文件中添加以下代码行。

custom_app/hooks.py

page_js = {"background_jobs": "public/js/custom_background_jobs.js"}

声音

Frappe 附带了一组音频通知,用于成功操作、文档提交、错误等事件。你可以使用 sounds 钩子添加自己的声音。

app/hooks.py

sounds = [
    {"name": "ping", "src": "/assets/app/sounds/ping.mp3", "volume": 0.2}
]

你可以使用客户端工具方法播放你添加的声音:

frappe.utils.play_sound("ping")

安装钩子

这些钩子允许你在应用安装之前和之后运行代码。例如,ERPNext 定义了这些。

# python module path
before_install = "app.setup.install.before_install"
after_install = "app.setup.install.after_install"
after_sync = "app.setup.install.after_sync"

app/setup/install.py

# will run before app is installed on site
def before_install():
    pass

# will run after app is installed on site
def after_install():
    pass

# will run after app fixtures are synced
def after_sync():
    pass

卸载钩子

这些钩子允许你在应用卸载之前和之后运行代码。

app/hooks.py

before_uninstall = "app.setup.uninstall.before_uninstall"
after_uninstall = "app.setup.uninstall.after_uninstall"

app/setup/uninstall.py

# will run before app is uninstalled from site
def before_uninstall():
    pass

# will run after app is uninstalled from site
def after_uninstall():
    pass

迁移钩子

这些钩子允许你在通过命令 bench --site sitename migrate 在站点上运行迁移之前和之后运行代码。

app/hooks.py

before_migrate = "app.migrate.before_migrate"
after_migrate = "app.migrate.after_migrate"

app/migrate.py

def after_migrate():
    # run code after site migration
    pass

构建钩子

此钩子允许你通过 bench build 命令扩展构建系统。它在完成 bench 上所有应用的构建后运行。构建钩子允许你在 bench build 结束时,在资源和翻译编译完成后运行代码。

after_build

在你自己的应用构建时运行。用于对你的应用的资源进行后处理。

app/hooks.py

after_build = "app.build.after_build"

app/build.py

def after_build() -> None:
    pass

after_app_build

在每次构建时运行,无论正在构建哪些应用。当您的应用为其他应用生成资源时使用它 – 例如,Frappe Studio 编译其他应用内附带的 studio 前端。

app/hooks.py

after_app_build = "papp.build.after_app_build"
**app/hooks.py**

<pre><code>after_app_build = "app.build.after_app_build"

app/build.py

def after_app_build(built_apps: list[str]) -> None:
    for app in built_apps:
        pass

**app/build.py**

python def after_app_build(built_apps: list[str]) -> None: for app in built_apps: pass # `built_apps` is the value of `–app`/`–apps`, or all apps on the bench when # `bench build` runs without arguments.

测试钩子

此钩子允许您在站点上运行测试之前执行代码。您可以使用此钩子向数据库添加种子数据,这些数据将可用于您的测试。

app/hooks.py

before_tests = "app.tests.before_tests"

app/migrate.py

def before_tests():
    # add seed data to the database
    pass

文件钩子

这些钩子允许您更改处理用户上传文件的实现方式。

app/hooks.py

before_write_file = "app.overrides.file.before_write"
write_file = "app.overrides.file.write_file"
delete_file_data_content = "app.overrides.file.delete_file"

app/overrides/file.py

# will run before file is written to disk
def before_write():
    pass

# will override the implementation of writing file to disk
# can be used to upload files to a CDN instead of writing
# the file to disk
def write_file():
    pass

# will override the implementation of deleting file from disk
# can be used to delete uploaded files from a CDN instead of
# deleting file from disk
def delete_file():
    pass

电子邮件钩子

这些钩子允许您更改默认电子邮件模块中发送电子邮件和设置默认发件人地址的实现方式。

app/hooks.py

override_email_send = "app.overrides.email.send"
get_sender_details = "app.overrides.email.get_sender_details"

默认情况下,frappe 在所有电子邮件中使用当前登录用户的姓名和 ID 作为发件人详细信息。这可以通过 get_sender_details 钩子覆盖。如果您想通过使用第三方服务器或应用发送电子邮件来扩展电子邮件模块的功能,则可以使用 override_email_send 钩子。此钩子会将所有电子邮件信息(发件人、收件人、内容(mime))发送到自定义应用中的函数。

app/overrides/email.py

# will be edited as "John Doe <[email protected]>"
def get_sender_details():
    return "John Doe", "[email protected]"

# self - EmailQueue object refrence for updating status
def send(self, sender, recipient, msg):
    # smtp or http request
    self.update_status("Sending")

注意:您需要根据邮件提供商/服务器返回的 webhook 响应,在自定义应用中处理电子邮件队列的状态更改

扩展启动信息

成功登录后,Desk 会被注入一个名为 bootinfo 的全局值字典。bootinfo 在 Javascript 中作为全局对象 frappe.boot 可用。

bootinfo 字典包含许多值,包括:

  • 系统默认值
  • 通知状态
  • 权限
  • 用户设置
  • 语言和时区信息

您可以通过 extend_bootinfo 钩子添加对您的应用有意义的全局值。

# python module path
extend_bootinfo = "app.boot.boot_session"

该方法以一个参数 bootinfo 调用,您可以直接在其上添加/更新值。

app/boot.py

def boot_session(bootinfo):
 bootinfo.my_global_key = "my_global_value"

现在,您可以在客户端代码的任何位置访问该值。

console.log(frappe.boot.my_global_key)

网站上下文

当门户页面被渲染时,会构建一个包含页面可能需要的所有变量的字典。这个字典也称为 context。您可以使用这些钩子在此字典中添加或修改值。

app/hooks.py

website_context = {
    "favicon": "/assets/app/image/favicon.png"
}
update_website_context = "app.overrides.website_context"

website_context 钩子是一个简单的键值对字典。使用此钩子进行简单的值覆盖。

对于更复杂的场景,您可以使用 update_website_context 钩子,因为它允许您在 Python 方法中操作上下文字典。该方法以一个参数调用,即 context 字典。您可以直接通过修改它来更改上下文,或者返回一个将与 context 合并的字典。

app/overrides.py

def website_context(context):
 context.my_key = "my_value"

网站控制器上下文

Frappe 附带标准网页,如 /404 和 /about。如果您想扩展这些页面的控制器上下文,可以使用 extend_website_page_controller_context 钩子。

app/hooks.py

extend_website_page_controller_context = {
    "frappe.www.404": "app.pages.context_404"
}

上述钩子配置将允许您扩展 404 页面的上下文,以便您可以添加自己的键或修改现有的键。

app/pages.py

def context_404(context):
    # context of the 404 page
    context.my_key = "my_value"

具有动态路由的网页

动态路由是其中包含动态值的路由。

示例:

/profile/<name>
</name>

这里“name”是动态部分,但渲染的是相同的个人资料页面。默认情况下,Frappe 支持来自“网页”文档类型的动态路由。要添加更多动态路由,可以使用 get_web_pages_with_dynamic_routes。

app/hooks.py

get_web_pages_with_dynamic_routes = "script.get_web_pages_with_dynamic_routes"

script.py

def get_web_pages_with_dynamic_routes():
 return [{
       "doctype": "Custom Web Page", // Doctype extended from WebsiteGenerator https://frappeframework.com/docs/user/en/guides/portal-development/generators
       "route": "/profile/
<name>",
       "name": "profile-page" // name of the web view document to render
    }, ...]
</name>

网站清除缓存

Frappe 框架缓存大量静态网页以便后续快速渲染。如果您创建了使用缓存值的网页,并且想要使缓存失效,此钩子就是执行此操作的地方。

app/hooks.py

website_clear_cache = "app.overrides.clear_website_cache"

该方法以一个参数 path 调用。当为单个路由清除缓存时,path 被设置,当为所有路由清除缓存时,None 被设置。如果您的缓存是特定于页面的,您需要处理这种情况。

app/overrides.py

def clear_website_cache(path=None):
    if path:
        # clear page related cache
    else:
        # clear all cache

网站重定向

网站重定向允许您定义从一个路由到另一个路由的重定向。当请求源 URL 时,Frappe 将生成 304 重定向响应并重定向到目标 URL。您可以重定向普通 URL,也可以使用正则表达式来匹配您的 URL。

app/hooks.py

website_redirects = [
    {"source": "/compare", "target": "/comparison"},
    {"source": "/docs(/.*)?", "target": "https://docs.tennismart.com/\1"},
    {"source": r'/items/item\?item_name=(.*)', "target": '/items/\1', match_with_query_string=True},
]

上述配置将导致以下重定向:

  • /compare 到 /comparison
  • /docs/getting-started 到 https://docs.tennismart.com/getting-started
  • /docs/help 到 https://docs.tennismart.com/help
  • /items/item?item_name=racket 到 https://docs.tennismart.com/items/racket

网站路由规则

网站路由规则允许您将 URL 映射到自定义控制器。这通常用于为页面生成简洁的 URL。

假设您希望 /projects 路由显示项目列表。这可以通过在 www 文件夹中创建 projects.html 和 projects.py 来实现。

您还希望 /project/<name></name> 路由显示项目页面,其中 name 是动态的。为此,您可以使用 website_route_rules 钩子。

app/hooks.py

website_route_rules = [
    {"from_route": "/projects/
<name>", "to_route": "app/projects/project"},
]
</name>

现在,您可以在 app/projects 文件夹中创建控制器文件。

app/projects/project.py

def get_context(context):
    project_name = frappe.form_dict.name
    project = frappe.get_doc("Project", project_name)
    context.project = project

app/projects/project.html

<h1>{{ project.title }}</h1>
<p>{{ project.description }}</p>

网站路径解析器

Frappe 会执行一些标准的路径解析,例如,任何对 “/profile” 的请求都会在内部转换为 “/me”。可以使用 website_path_resolver 来覆盖此行为。

app/hooks.py

website_path_resolver = "path.to.custom_resolver_method"

注意: 您的自定义解析方法将接收请求的路由,并应返回处理后的路由。

网站 404

当页面未找到时,Frappe 会渲染默认的 /404 路由。您可以使用 website_catch_all 钩子来更改此设置。

app/hooks.py

website_catch_all = "not_found"

上述配置将在发生 404 错误时渲染 /not_found。您需要自行实现模板 www/not_found.html 和控制器 www/not_found.py。

默认首页

首页是当您访问站点根 URL(/)时渲染的页面。有多种方法可以配置默认渲染哪个页面作为首页。

默认情况下,首页是 index。因此,frappe 将尝试从 www 文件夹渲染 index.html。这可以使用 homepage 钩子来覆盖。

app/hooks.py

homepage = "homepage"

上述配置将加载 www/homepage.html 作为默认首页。

您还可以通过使用 role_home_page 钩子来设置基于角色的首页。

app/hooks.py

role_home_page = {
    "Customer": "orders",
    "Supplier": "bills"
}

上述配置将使 /orders 成为具有 客户 角色的用户的默认首页,并使 /bills 成为具有 供应商 角色的用户的默认首页。

您还可以通过使用 get_website_user_home_page 钩子对逻辑进行更精细的控制。

app/hooks.py

get_website_user_home_page = "app.website.get_home_page"

app/website.py

def get_home_page(user):
    if is_projects_user(user):
        return "projects"
    if is_partner(user):
        return "partner-dashboard"
    return "index"

如果所有这些钩子都已定义,则 get_website_user_home_page 的优先级将高于其他钩子,而 role_home_page 的优先级将高于 homepage。

门户侧边栏

某些门户视图会显示带有链接的侧边栏,以便快速跳转到页面。这些侧边栏项目可以通过钩子进行自定义。

app/hooks.py

portal_menu_items = [
    {"title": "Dashboard", "route": "/dashboard", "role": "Customer"},
    {"title": "Orders", "route": "/orders", "role": "Customer"},
]

上述配置将为具有客户角色的用户添加两个侧边栏链接。

这些侧边栏项目在您的应用程序中是硬编码的,因此无法从 Desk 进行自定义。例如,如果您想临时隐藏某个侧边栏链接,则必须修改代码。

还有另一个名为 standard_portal_menu_items 的钩子允许您执行此操作。在 standard_portal_menu_items 钩子中设置的侧边栏链接将与数据库同步。

app/hooks.py

standard_portal_menu_items = [
    {"title": "Dashboard", "route": "/dashboard", "role": "Website Manager"},
    {"title": "Orders", "route": "/orders", "role": "Website Manager"},
]

上述配置会将侧边栏项目同步到门户设置,之后任何系统用户都可以对其进行编辑。

品牌 HTML

此钩子允许您自定义网站导航栏中的品牌标志。

app/hooks.py

brand_html = '<div><img src="tennismart.png"> TennisMart</div>'

如果定义了 brand_html,它将覆盖导航栏中的默认品牌 HTML。除非您想对其进行版本控制,否则不建议使用钩子来更改品牌标志,否则您可以使用网站设置来更改它。

基础模板

渲染网页时,默认会扩展 templates/base.html。您可以通过覆盖 base_template 钩子来覆盖基础模板。

app/hooks.py

base_template = "app/templates/my_custom_base.html"

您还可以根据路由自定义基础模板。例如,如果您想为所有以 docs/* 开头的路由使用不同的基础模板,则可以使用 base_template_map 钩子。键必须是匹配路由的正则表达式。所有其他路由将回退到默认基础模板。

app/hooks.py

base_template_map = {
    r"docs.*": "app/templates/doc_template.html"
}

集成

这些钩子允许您自定义 Frappe 中第三方集成的行为。

Braintree 成功页面

此钩子允许您在 Braintree 交易成功支付后覆盖默认的重定向 URL。

app/hooks.py

braintree_success_page = "app.integrations.braintree_success_page"

该方法使用一个参数 data 调用,该参数包含付款的元数据。

app/integrations.py

def braintree_success_page(data):
    # data.reference_doctype
    # data.reference_docname
    return "/thank-you"

日历

日历钩子是一个文档类型名称列表,这些名称在 Desk 的日历页面中显示为菜单项,以便快速导航。

app/hooks.py

calendars = ["Appointment"]

清除缓存

此钩子允许您在 Frappe 清除全局缓存时,清除您应用特定的缓存值。

app/hooks.py

clear_cache = "app.cache.clear_cache"

您可以使用此钩子来清除您应用特定的缓存。该方法在调用时不带任何参数。

app/cache.py

def clear_cache():
    frappe.cache().hdel("app_specific_cache")

如果您想设置 Frappe 发送的所有邮件的默认页脚,可以使用 default_mail_footer 钩子。

app/hooks.py

default_mail_footer = """

<div>
 Sent via <a href="https://tennismart.com" target="_blank">TennisMart</a>
</div>
"""

现在,所有邮件的页脚都将显示 通过 TennisMart 发送。

会话钩子

这些钩子在用户登录生命周期中被触发。on_login 在成功登录后立即触发,on_session_creation 在会话设置完成后触发,on_logout 在用户注销后触发。

app/hooks.py

on_login = "app.overrides.successful_login"
on_session_creation = "app.overrides.allocate_free_credits"
on_logout = "app.overrides.clear_user_cache"

该方法将使用一个参数 login_manager 被调用。

app/overrides.py

def allocate_free_credits(login_manager):
    # allocate free credits to frappe.session.user
    pass

认证钩子

这些钩子在请求认证期间被触发。可以在此处验证自定义标头、授权标头,用户通过 frappe.set_user() 被验证并映射到请求。使用 frappe.request 和 frappe.* 来验证请求并映射用户。

app/hooks.py

auth_hooks = ["app.overrides.validate_custom_jwt"]

该方法将在请求认证期间被调用。

app/overrides.py

def validate_custom_jwt():
    # validate jwt from header, verify signature, set user from jwt.
    pass

使用此方法检查传入的请求标头,验证标头并将用户映射到请求。如果标头验证失败,请勿抛出错误以继续使用其他钩子。未经验证的请求默认被视为“访客”请求。您可以使用第三方服务器、共享数据库或任何其他选择来验证和映射请求与用户。

固定数据

固定数据是当您安装和更新站点时,通过 JSON 文件同步的数据库记录。

假设您希望在安装应用时在数据库中创建一组类别。为此,请在您的本地站点中创建这组类别,并将文档类型名称添加到 fixtures 钩子中。

fixtures = [
    # export all records from the Category table
    "Category"
]

现在,运行以下命令:

bench --site sitename export-fixtures

此命令将为每个文档类型创建一个 JSON 文件,其中包含生成记录列表所需的数据。您可以通过创建一个新站点并在该站点上安装您的应用来测试此功能。

您还可以为导出记录添加条件。

fixtures = [
    # export all records from the Category table
    "Category",
    # export only those records that match the filters from the Role table
    {"dt": "Role", "filters": [["role_name", "like", "Admin%"]]},
]

某些字段仅供内部使用。系统会自动设置并保持这些字段的最新状态。这些字段不会被导出:modified_by、creation、owner、idx、lft 和 rgt。对于子表记录,以下字段不会被导出:docstatus、doctype、modified 和 name。

文档钩子

修改列表查询

您可以通过使用 permission_query_conditions 钩子添加自定义匹配条件,来自定义 DocType 记录列表的查询方式。此匹配条件必须是 SQL 查询的有效 WHERE 子句片段。

app/hooks.py

permission_query_conditions = {
    "ToDo": "app.permissions.todo_query",
}

该方法使用单个参数 user 被调用,该参数可以是 None。该方法应返回一个字符串,该字符串是有效的 SQL WHERE 子句。

app/permissions.py

def todo_query(user):
    if not user:
        user = frappe.session.user
    # todos that belong to user or assigned by user
    return "(`tabToDo`.owner = {user} or `tabToDo`.assigned_by = {user})".format(user=frappe.db.escape(user))

现在,如果您使用 frappe.db.get_list 方法,您的 WHERE 子句将被附加到查询中。

todos = frappe.db.get_list("ToDo", debug=1)

# output
'''
select `tabToDo`.`name`
from `tabToDo`
where ((`tabToDo`.owner = '[email protected]' or `tabToDo`.assigned_by = '[email protected]'))
order by `tabToDo`.`modified` DESC
'''

此钩子只会影响 frappe.db.get_list 方法的结果,而不会影响 > frappe.db.get_all。

文档权限

您可以使用 has_permission 钩子修改任何 DocType 的 doc.has_permission 文档方法的行为,并添加自定义权限检查逻辑。

app/hooks.py

has_permission = {
    "Event": "app.permissions.event_has_permission",
}

该方法将接收 doc、user 和 permission_type 作为参数。它应返回 True 或一个 False 值。如果返回 None,它将回退到默认行为。

app/permissions.py

def event_has_permission(doc, user=None, permission_type=None):
    # when reading a document allow if event is Public
    if permission_type == "read" and doc.event_type == "Public":
        return True

    # when writing a document allow if event owned by user
    if permission_type == "write" and doc.owner == user:
        return True

    return False

扩展 DocType 类

注意:此功能在 v16+ 中可用

您可以使用 extend_doctype_class 钩子扩展标准文档类型的类。此钩子允许您向现有的 DocType 类添加属性、方法和功能,而无需完全覆盖它们,从而使多个应用能够扩展同一个 DocType 类。

app/hooks.py

extend_doctype_class = {
    "Address": ["app.extensions.address.AddressMixin"]
}

app/extensions/address.py

from frappe.model.document import Document

class AddressMixin(Document):
    @property
    def full_address(self):
        return f"{self.address_line1}, {self.city}, {self.country}"

    def custom_validation(self):
        # Custom validation logic
        pass

    def validate(self):
        super.validate()
        self.custom_validation()

多个扩展

您可以为同一个 DocType 定义多个扩展。例如,ValidationMixin 可以用于 联系人 以及 地址,而 GeocodingMixin 仅用于 地址。

app/hooks.py

extend_doctype_class = {
    "Address": [
        "app.extensions.address.GeocodingMixin",
        "app.extensions.common.ValidationMixin"
    ],
    "Contact": [
        "app.extensions.common.ValidationMixin"
    ]
}

钩子解析顺序

当多个应用为同一个 DocType 定义扩展时,扩展将按照钩子解析顺序应用。如果应用按 frappe、app1、app2 的顺序解析,最终的类将是:

class ExtendedAddress(App2Mixin, App1Mixin, Address):
    pass

与 override_doctype_class 的交互

extend_doctype_class 钩子在 [override_doctype_class](#override-doctype-class) 之上工作。如果同时定义了这两个钩子,扩展将应用于被覆盖的类,而不是基类。

例如,如果 ERPNext 覆盖了地址类,而自定义应用对其进行了扩展:

# ERPNext overrides Address
override_doctype_class = {
    "Address": "erpnext.setup.doctype.address.address.Address"
}

# Custom apps extend it
extend_doctype_class = {
    "Address": ["app1.extensions.Prop1Mixin", "app2.extensions.Prop2Mixin"]
}

# Final class becomes:
class ExtendedAddress(Prop2Mixin, Prop1Mixin, <a href="https://erpnext.yuannext.com">ERPNext</a>Address):
    pass

此钩子非常适合添加虚拟字段、计算属性和自定义方法,而不会干扰核心功能。当您只需要添加功能而不是替换功能时,建议使用此钩子而不是 [override_doctype_class](#override-doctype-class)。

覆盖 DocType 类

您可以使用 override_doctype_class 钩子来覆盖/扩展标准 DocType 的类。与 [extend_doctype_class](#extend-doctype-class) 不同,此钩子会完全替换原始类。当多个应用覆盖同一个 DocType 类时,这可能会导致问题。在 v16+ 中,建议改用 [extend_doctype_class](#extend-doctype-class)。

app/hooks.py

override_doctype_class = {
    "ToDo": "app.overrides.todo.CustomToDo"
}

app/overrides/todo.py

from frappe.desk.doctype.todo.todo import ToDo

class CustomToDo(ToDo):
    def on_update(self):
        self.my_custom_code()
        super().on_update()

    def my_custom_code(self):
        pass

建议您扩展 DocType 的标准类,否则您将不得不自行实现所有核心功能。

覆盖表单脚本

您可以使用 doctype_js 钩子来覆盖/扩展标准表单脚本。

app/hooks.py

doctype_js = {
    "ToDo": "public/js/todo.js",
}

app/public/js/todo.js

frappe.ui.form.on("Todo", {
    refresh: function(frm) {
        frm.trigger("my_custom_code");
    },
    my_custom_code: function(frm){
        console.log(frm.doc.name)
    }
});

在 app/public/todo.js 中定义的事件/函数将扩展 ToDo DocType 的标准表单脚本中的事件/函数。

CRUD 事件

您可以使用 doc_events 钩子来挂钩任何 DocType 的各种 CRUD 事件。

app/hooks.py

doc_events = {
    "*": {
        # will run after any DocType record is inserted into database
        "after_insert": "app.crud_events.after_insert_all"
    },
    "ToDo": {
        # will run before a ToDo record is inserted into database
        "before_insert": "app.crud_events.before_insert_todo",
    }
}

该方法将接收文档和方法名称作为参数。

app/crud_events.py

def after_insert_all(doc, method=None):
    pass

def before_insert_todo(doc, method=None):
    pass

有关所有可用钩子的列表,请参阅控制器钩子 >。

覆盖白名单方法

白名单方法是在 REST 端点上可访问并被客户端使用的 Python 方法。您可以使用 override_whitelisted_methods 钩子来覆盖核心框架中标准白名单方法。

app/hooks.py

override_whitelisted_methods = {
    "frappe.client.get_count": "app.whitelisted.custom_get_count"
}

该方法应具有与原始方法相同的签名。

app/whitelisted.py

def custom_get_count(doctype, filters=None, debug=False, cache=False):
    # your custom implementation of the standard get_count method provided by frappe
    pass

要在删除文档时忽略对特定 DocType 的链接,您可以在 ignore_links_on_delete 钩子中指定它们,如下所示:

app/hooks.py

ignore_links_on_delete = ["Communication", "ToDo"]

表单时间线

文档表单视图的时间线部分显示了对该文档执行的操作的审计跟踪,例如查看、值更改、评论和相关通信等。

除了这些标准操作之外,有时您可能需要添加自己的自定义操作。您可以通过 additional_timeline_content 钩子来实现。

additional_timeline_content: {
    # show in each document's timeline
    "*": ["app.timeline.all_timeline"]
    # only show in ToDo's timeline
    "ToDo": ["app.timeline.todo_timeline"]
}

该方法将接收 doctype 和 docname 作为参数。您可以执行查询并返回与该文档相关的操作,作为字典列表,如示例所示。列表中的每个字典必须有一个 creation 值,该值将用于对时间线中的项目进行排序。

def todo_timeline(doctype, docname):
    # this method should return a list of dicts
    return [
        {
             # this will be used to sort the content in the timeline
            "creation": "22-05-2020 18:00:00",
            # this JS template will be rendered in the timeline
            "template": "custom_timeline_template",
            # this data will be passed to the template.
            "template_data": {"key": "value"},
        },
        ...
    ]

调度器事件

您可以使用 scheduler_events 钩子通过调度器事件在后台定期运行任务。

app/hooks.py

scheduler_events = {
    "hourly": [
        # will run hourly
        "app.scheduled_tasks.update_database_usage"
    ],
}

app/scheduled_tasks.py

def update_database_usage():
    pass

在 hooks.py 中更改任何计划事件后,您需要运行 bench migrate 才能使更改生效。

可用事件

  • hourly、daily、weekly、monthly

这些事件将分别每小时、每天、每周和每月触发一次。

  • hourly_long、daily_long、weekly_long、monthly_long

与上述相同,但这些作业在长工作进程中运行,适用于长时间运行的作业。

  • all

all 事件每 60 秒触发一次。这可以通过 common_site_config.json 中的 scheduler_tick_interval 键进行配置。

  • cron

一个有效的 cron 字符串,可由 croniter 解析。

使用示例:

scheduler_events = {
    "daily": [
        "app.scheduled_tasks.manage_recurring_invoices"
    ],
    "daily_long": [
        "app.scheduled_tasks..take_backups_daily"
    ],
    "cron": {
        "15 18 * * *": [
            "app.scheduled_tasks..delete_all_barcodes_for_users"
        ],
        "*/6 * * * *": [
            "app.scheduled_tasks..collect_error_snapshots"
        ],
        "annual": [
            "app.scheduled_tasks.collect_error_snapshots"
        ]
    }
}

Jinja 自定义

Frappe 在 Jinja 模板中提供了一系列全局实用方法。要添加您自己的方法和过滤器,您可以使用 jinja 钩子。

app/hooks.py

jinja = {
    "methods": [
        "app.jinja.methods",
        "app.utils.get_fullname"
    ],
    "filters": [
        "app.jinja.filters",
        "app.utils.format_currency"
    ]
}

app/jinja/methods.py

def sum(a, b):
    return a + b

def multiply(a, b):
    return a * b

如果路径是模块路径,则该模块中的所有方法都将被添加。

app/utils.py

def get_fullname(user):
    first_name, last_name = frappe.db.get_value("User", user, ["first_name", "last_name"])
    return first_name + " " + last_name

def format_currency(value, currency):
    return currency + " " + str(value)

现在,您可以在 Jinja 模板中使用这些实用程序,如下所示:

<h1>Hi, {{ get_fullname(frappe.session.user) }}</h1>
<p>Your account balance is {{ account_balance | format_currency("INR") }}</p>
<p>1 + 2 = {{ sum(1, 2) }}</p>

防止链接文档自动取消

要防止特定 DocType 的文档在取消任何链接文档时被自动取消,您可以使用 auto_cancel_exempted_doctypes 钩子。

app/hooks.py

auto_cancel_exempted_doctypes = ["Payment Entry"]

在上面的示例中,如果任何与付款条目链接的文档(例如销售发票)被取消,它将跳过链接的付款条目文档的自动取消。

通知配置

通知配置钩子用于自定义工作台中通知下拉列表中显示的项目。它可以通过 notification_config 钩子进行配置。

app/hooks.py

notification_config = "app.notification.get_config"

该方法在没有任何参数的情况下被调用。

app/notification.py

def get_config():
 return {
        "for_doctype": {
            "Issue": {"status":"Open"},
            "Issue": {"status":"Open"},
        },
        "for_module_doctypes": {
            "ToDo": "To Do",
            "Event": "Calendar",
            "Comment": "Messages"
        },
        "for_module": {
            "To Do": "frappe.core.notifications.get_things_todo",
            "Calendar": "frappe.core.notifications.get_todays_events",
            "Messages": "frappe.core.notifications.get_unread_messages"
        }
    }

上述配置包含三个部分:

  1. 上述配置中的 for_doctype 部分会将状态为“开启”的“问题”或“客户问题”标记为未读
  2. for_module_doctypes 将文档类型映射到模块的未读计数。
  3. for_module 将模块映射到获取其未读计数的函数。这些函数在调用时不带任何参数。

所需应用

在构建应用时,您可能会创建基于其他应用构建的应用。为确保在有人安装您的应用时同时安装依赖应用,您可以使用 required_apps 钩子。

app/hooks.py

required_apps = ["erpnext"]

上述配置将确保在有人安装您的应用时,erpnext 也会被安装。

用户数据保护与隐私

Frappe 内置了个人数据下载和个人数据删除等用户数据隐私功能。哪些数据构成个人数据可由应用发布者在应用的 hooks.py 文件中以 user_data_fields 的形式定义。

app/hooks.py

user_data_fields = [
    {"doctype": "Access Log"},
    {"doctype": "Comment", "strict": True},
    {
        "doctype": "Contact",
        "filter_by": "email_id",
        "rename": True,
    },
    {"doctype": "Contact Email", "filter_by": "email_id"},
    {
        "doctype": "File",
        "filter_by": "attached_to_name",
        "redact_fields": ["file_name", "file_url"],
    },
    {"doctype": "Email Unsubscribe", "filter_by": "email", "partial": True},
]

包含用户数据的文档类型应按照上述格式映射到此钩子下。当用户提出数据删除或下载请求时,将利用此钩子映射到指定的文档类型。可用于修改文档的选项如下:

字段 说明
doctype 包含用户数据的文档类型。
filter_by 用于筛选文档的文档字段。如果未设置,默认为 owner。
partial 如果设置,将解析所有文本字段并删除用户的姓名和用户名引用。
redact_fields 需要删除的字段。如果未指定,则视为对所有文本字段进行部分数据删除。
rename 如果文档名称包含用户数据,设置此字段以重命名文档,使其匿名化。
strict 如果设置为 True,将从当前文档类型的所有文档中删除任何用户数据。如果未设置,默认为 False,这意味着仅筛选用户为所有者的文档。

注意:个人数据下载仅使用 user_data_fields 中定义的文档类型和筛选字段。

相关主题:

  1. 个人数据删除
  2. 个人数据下载

注册表单模板

如果您想向注册表单添加额外字段,可以使用此钩子。创建一个包含自定义注册表单的模板文件。将此模板传递给自定义注册钩子。

signup_form_template = "school/templates/signup-form.html"

注意:如果您希望在注册表单中添加自定义字段,则需要在用户文档类型中添加额外字段。您必须使用固定数据(fixtures)来添加这些字段。此外,您还需要为这个注册表单编写自己的提交处理程序,并在服务器端编写一个用于注册用户的函数。这样,您还可以为您添加的自定义字段编写验证逻辑。

短信钩子

这些钩子允许您通过集成第三方短信服务提供商或实现自定义短信逻辑,来自定义 Frappe 中的短信发送功能。

发送短信

此钩子允许您覆盖通知、群发短信和其他常规短信功能的默认短信发送逻辑。

app/hooks.py

send_sms = "app.overrides.sms.send_sms"

调用该方法时会传入短信详情,包括收件人、消息内容和其他元数据。

app/overrides/sms.py

def send_sms(receiver_list, msg, sender=None, success_msg=True):
    """
    Override default SMS sending logic

    Args:
        receiver_list: List of mobile numbers or single mobile number
        msg: SMS message content
        sender: Sender ID (optional)
        success_msg: Whether to show success message (optional)
    """

    # Send SMS via custom provider
    response = custom_sms_provider.send_sms(
        to=receiver_list,
        message=msg,
        from_number=sender
    )

    if response.status_code == 200:
        frappe.msgprint(_("SMS sent successfully"))
        return True
    else:
        frappe.throw(_("Failed to send SMS"))
        return False

通过短信发送令牌

此钩子允许您覆盖双因素认证和移动登录流程中的短信 OTP 发送逻辑。

app/hooks.py

send_token_via_sms = "app.overrides.sms.send_token_via_sms"

调用该方法时会传入用于认证目的的 OTP 详情。

app/overrides/sms.py

def send_token_via_sms(otpsecret, token=None, phone_no=None):
   """
    Generate OTP and send using local send_otp function.
    :param otpsecret: OTP secret for generating HOTP
    :param token: Token to use for HOTP generation
    :param phone_no: Phone number to send OTP to
    """
    if not phone_no:
        return False
    try:
        hotp = pyotp.HOTP(otpsecret)
        otp_code = hotp.at(token_int)
        result = send_otp(
            number=phone_no,
            otp_length=len(otp_code),
            otp_expiry=5,  # 5 minutes expiry
            otp=otp_code
        )
        return result.get("success", False)
    except Exception as e:
        frappe.log_error(
            message=f"Failed to send OTP: {str(e)}",
            title="OTP Error"
        )
        return False

可用钩子列表

钩子名称 说明
additional_timeline_content 表单时间线
after_install 安装钩子
after_migrate 迁移钩子
after_sync 安装钩子
app_include_css 工作台资源
app_include_js 工作台资源
app_logo_url 应用元数据
app_title 应用元数据
auto_cancel_exempted_doctypes 防止自动取消
base_template_map 基础模板
base_template 基础模板
before_install 安装钩子
before_migrate 迁移钩子
before_tests 测试钩子
before_write_file 文件钩子
bot_parsers 已弃用
braintree_success_page Braintree 成功页面
brand_html 品牌 HTML
calendars 日历
clear_cache 清除缓存
communication_doctypes
default_mail_footer 默认邮件页脚
delete_file_data_content 文件钩子
doc_events 文档增删改查事件
doctype_js 覆盖表单脚本
domains
dump_report_map 已弃用
extend_bootinfo 扩展启动信息
extend_website_page_controller_context 网站控制器上下文
filters_config
fixtures 测试数据
get_site_info
get_translated_dict
get_website_user_home_page 默认首页
get_web_pages_with_dynamic_routes 带动态路由的网页
has_permission 文档权限
has_website_permission
home_page 默认首页
jenv Jinja 自定义
leaderboards
look_for_sidebar_json
make_email_body_message
notification_config 通知配置
on_login 会话钩子
on_logout 会话钩子
on_print_pdf 打印时
on_session_creation 会话钩子
extend_doctype_class 扩展 DocType 类
override_doctype_class 覆盖 DocType 类
override_doctype_dashboards
override_whitelisted_methods 覆盖白名单方法
ignore_links_on_delete 删除时忽略链接
permission_query_conditions 修改列表查询
portal_menu_items 门户侧边栏
required_apps 所需应用
role_home_page 默认首页
scheduler_events 计划任务事件
setup_wizard_complete
setup_wizard_exception
setup_wizard_requires
setup_wizard_stages
setup_wizard_success
signup_form_template 注册表单模板
sounds 提示音
standard_portal_menu_items 门户侧边栏
standard_queries 标准查询
send_sms 短信钩子
send_token_via_sms 短信钩子
template_apps
translated_languages_for_website
translator_url
treeviews 默认使用树形视图(而非列表视图)作为默认视图的 DocType
update_website_context 网站上下文
user_privacy_documents 已弃用(改用 user_data_fields 钩子)
user_data_fields 用户数据保护与隐私
web_include_css 门户资源
web_include_js 门户资源
website_catch_all 网站 404 页面
website_clear_cache 网站缓存清理
website_context 网站上下文
website_generators 已弃用(请改用 DocType 中的“具有网页视图”选项)
website_redirects 网站重定向
website_route_rules 网站路由规则
website_user_home_page 已弃用(改用 homepage 钩子)
welcome_email
write_file_keys 已弃用
write_file 文件钩子

搜索

在 Frappe 中进行搜索由 Search 模块管理。它是 Whoosh 的封装,Whoosh 是一个用 Python 编写的全文搜索库。

您可以扩展 FullTextSearch 类,为特定需求创建搜索类。例如,WebsiteSearch 是一个用于索引公开网页并暴露搜索功能的封装。

FullTextSearch 类

每个 FullTextSearch(FTS)实例都持有一个由类本身定义的 Schema。这意味着,特定的 FTS 实现会有其特定的 schema。如果您希望使用不同的 schema 进行索引,可以创建新的实现。除此之外,FTS 类还提供了其他控制器,用于创建、更新和查询索引。

扩展 FTS 类

初始化基于 FTS 的类时,您需要提供一个索引名称。实例化时,会初始化以下参数:

  • index_name:提供的索引名称。
  • index_path:索引在站点文件夹中的路径。
  • schema:由 get_schema 函数返回。
  • id:用于在索引中识别文档的 ID。

实例化后,您可以运行 build 函数。它会从 get_items_to_index 获取所有文档,这些文档是符合定义 schema 的 frappe._dict(frappe 字典)列表。然后,这些文档会被添加到索引中并写入文件。

您可以使用 FTS 类的 search 方法搜索索引。这些函数在 API 参考文档中有详细说明。

博客的示例实现如下所示:

class BlogWrapper(FullTextSearch):
 # Default Schema
 # def get_schema(self):
 # return Schema(name=ID(stored=True), content=TEXT(stored=True))

 # def get_id(self):
 # return "name"

 def get_items_to_index(self):
 docs = []
 for blog_name in get_all_blogs():
 docs.append(get_document_to_index(blog_name))
 return docs

 def get_document_to_index(self, name):
 blog = frappe.get_doc("Blog Post", name)
 return frappe._dict(name=name, content=blog.content)

 def parse_result(self, result):
 return result["name"]
  • get_items_to_index:获取所有需要索引的路由,包括 www/ 目录下的静态页面以及已发布文档的路由。
  • get_document_to_index:渲染页面并使用 BeautifulSoup 进行解析。
  • parse_result:所有搜索结果都通过此函数进行解析。

好的,请发送需要翻译的英文标题。

在这里,让我们来看看 Frappe 中响应是如何构建的,以及你如何在你的
Frappe 应用或脚本中使用它们。

如果你已经阅读过路由器
文档,你可能已经注意到 Frappe 内部用来根据内容类型
构建响应的 build_response 函数。定义此行为的逻辑属于
frappe.utils.response
模块的一部分,其中 build_response 是该模块的核心内容。

def build_response(response_type=None):
 if "docs" in frappe.local.response and not frappe.local.response.docs:
 del frappe.local.response["docs"]

 response_type_map = {
 "csv": as_csv,
 "txt": as_txt,
 "download": as_raw,
 "json": as_json,
 "pdf": as_pdf,
 "page": as_page,
 "redirect": redirect,
 "binary": as_binary,
 }

 return response_type_map[frappe.response.get("type") or response_type]()

上面的代码片段展示了 build_response 的当前实现,
它映射了不同的函数,这些函数充当不同内容类型的处理器。
让我们更深入地了解一下 Frappe v13 中针对 “download”
response_type 的响应处理器。

def as_raw():
 response = Response()
 response.mimetype = (
 frappe.response.get("content_type")
 or mimetypes.guess_type(frappe.response["filename"])[0]
 or "application/unknown"
 )
 response.headers["Content-Disposition"] = (
 f'{frappe.response.get("display_content_as", "attachment")};'
 f' filename="{frappe.response["filename"].replace(" ", "_")}"'
 ).encode("utf-8")
 response.data = frappe.response["filecontent"]
 return response

根据 Content-Disposition 头的值,
接收响应的浏览器可能会有不同的行为。如果未设置,该值默认为
“attachment”。

如果 frappe.response.display_content_as 设置为 “inline”,则表示
内容预期在浏览器中内联显示,即作为
网页或网页的一部分显示,而 “attachment” 则表示内容
将被下载并保存在本地。

要创建一个可以直接下载所需文件的 API 端点,
你可以编写类似以下代码来实现直接下载文件。

@frappe.whitelist()
def download(name):
 file = frappe.get_doc("File", name)
 frappe.response.filename = file.file_name
 frappe.response.filecontent = file.get_content()
 frappe.response.type = "download"
 frappe.response.display_content_as = "attachment"

工具函数

Frappe 框架附带各种实用函数,用于处理常见的操作,例如站点特定的日期时间管理、日期和货币格式化、PDF 生成等等。

这些实用方法可以从 frappe.utils 模块(及其嵌套模块,如 frappe.utils.logger 和 frappe.utils.data)中导入到您的 Frappe 应用的任何 Python 文件中。此列表并非详尽无遗,您可以查看框架代码库以了解可用的内容。

now(当前时间)

now()

返回当前日期时间,格式为 yyyy-mm-dd hh:mm:ss

from frappe.utils import now

now() # '2021-05-25 06:38:52.242515'

getdate(获取日期)

getdate(string_date=None)

将 string_date(yyyy-mm-dd)转换为 datetime.date 对象。如果未提供输入,则返回当前日期。如果 string_date 是无效的日期字符串,则会抛出异常。

from frappe.utils import getdate

getdate() # datetime.date(2021, 5, 25)
getdate('2000-03-18') # datetime.date(2000, 3, 18)

today(今天)

today()

返回当前日期,格式为 yyyy-mm-dd。

from frappe.utils import today

today() # '2021-05-25'

add_to_date(日期加减)

add_to_date(date, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, as_string=False, as_datetime=False)

`date`: A string representation or `datetime` object, uses the current `datetime` if `None` is passed
`as_string`: Return as string
`as_datetime`: If `as_string` is True and `as_datetime` is also True, returns a `datetime` string otherwise just the `date` string.

此函数在进行日期/日期时间增减时非常方便,例如,从特定日期/日期时间加上或减去一定数量的天数。

from datetime import datetime # from python std library
from frappe.utils import add_to_date

today = datetime.now().strftime('%Y-%m-%d')
print(today) # '2021-05-21'

after_10_days = add_to_date(datetime.now(), days=10, as_string=True)
print(after_10_days) # '2021-05-31'

add_to_date(datetime.now(), months=2) # datetime.datetime(2021, 7, 21, 15, 31, 18, 119999)
add_to_date(datetime.now(), days=10, as_string=True, as_datetime=True) # '2021-05-31 15:30:23.757661'
add_to_date(None, years=6) # datetime.datetime(2027, 5, 21, 15, 32, 31, 652089)

date_diff(日期差)

date_diff(date_2, date_1)

返回给定两个日期之间的天数差。

from frappe.utils import add_to_date, today, date_diff
date_1 = today()
date_2 = add_to_date(date_1, days=10)

print(date_diff(date_2, date_1)) #10

days_diff(天数差)

days_diff(date_2, date_1)

返回给定两个日期之间的天数差。

from frappe.utils import add_to_date, today, days_diff
date_1 = today()
date_2 = add_to_date(date_1, days=10)

print(days_diff(date_2, date_1)) #10

month_diff(月份差)

month_diff(date_2, date_1)

返回给定两个日期之间的月份差。

from frappe.utils import add_to_date, month_diff
date_1 = "2024-07-01"
date_2 = add_to_date(date_1, days=60)

print(month_diff(date_2, date_1)) #2

pretty_date(友好日期)

pretty_date(iso_datetime)

接受一个 ISO 时间,并返回一个字符串,表示该日期距今有多长时间。这在即时通讯等通信应用中非常常见。

from frappe.utils import pretty_date, now, add_to_date

pretty_date(now()) # 'just now'

# Some example outputs:

# 1 hour ago
# 20 minutes ago
# 1 week ago
# 5 years ago

format_duration(格式化时长)

format_duration(seconds, hide_days=False)

将给定的以秒(浮点数)为单位的时长值转换为时长格式。

from frappe.utils import format_duration

format_duration(50) # '50s'
format_duration(10000) # '2h 46m 40s'
format_duration(1000000) # '11d 13h 46m 40s'

# Convert days to hours
format_duration(1000000, hide_days=True) # '277h 46m 40s'

comma_and(逗号和)

comma_and(some_list, add_quotes=True)

给定一个列表或元组 some_list,返回格式为 1st item, 2nd item, .... and last item 的字符串。此函数使用 frappe._,因此您不必担心单词 and 的翻译问题。如果 add_quotes 为 False,则返回不带引号的项目,否则返回带引号的项目。如果作为参数传递的 some_list 的类型不是列表或元组,则原样返回它(some_list)。

from frappe.utils import comma_and

comma_and([1, 2, 3]) # "'1', '2' and '3'"
comma_and(['Apple', 'Ball', 'Cat'], add_quotes=False) # 'Apple, Ball and Cat'
comma_and('abcd') # 'abcd'

还有一个 comma_or 函数,它与 comma_and 类似,只是分隔符不同,在 comma_or 的情况下,分隔符是 or。

money_in_words(金额转大写)

money_in_words(number, main_currency=None, fraction_currency=None)

`number`: A floating point money amount
`main_currency`: Uses this as the main currency. If not given, tries to fetch from default settings or uses `INR` if not found there.

此函数返回带有货币和辅币单位的字符串(金额大写形式)。

from frappe.utils import money_in_words

money_in_words(900) # 'INR Nine Hundred and Fifty Paisa only.'
money_in_words(900.50) # 'INR Nine Hundred and Fifty Paisa only.'
money_in_words(900.50, 'USD') # 'USD Nine Hundred and Fifty Centavo only.'
money_in_words(900.50, 'USD', 'Cents') # 'USD Nine Hundred and Fifty Cents only.'

validate_json_string(验证 JSON 字符串)

validate_json_string(string)

如果给定的 string 是有效的 JSON(JavaScript 对象表示法)字符串,则抛出 frappe.ValidationError。您可以使用 try-except 块来处理对此函数的调用,如下面的代码片段所示。

import frappe
from frappe.utils import validate_json_string

# No Exception thrown
validate_json_string('[]')
validate_json_string('[{}]')
validate_json_string('[{"player": "one", "score": 199}]')

try:
    # Throws frappe.ValidationError
    validate_json_string('invalid json')
except frappe.ValidationError:
    print('Not a valid JSON string')

random_string(随机字符串)

random_string(length)

此函数生成一个包含 length 个字符的随机字符串。这在某些情况下对于生成加密或秘密信息非常有用。

from frappe.utils import random_string

random_string(40) # 'mcrLCrlvkUdkaOe8m5xMI8IwDB8lszwJsWtZFveQ'
random_string(6) # 'htrB4L'
random_string(6) #'HNRirG'

mask_string(掩码字符串)

mask_string(input_string, mask_char="*", show_first=4, show_last=3)

出于隐私保护目的,通过隐藏中间字符来掩码字符串,同时显示可配置的首尾字符以用于识别。

from frappe.utils import mask_string

mask_string("1234567890")  # "1234***890"
mask_string("12345")       # "******"
mask_string("1234567890", mask_char="#", show_first=2, show_last=2)  # "12####90"

unique(去重)

unique(seq)

seq:一个可迭代对象 / 序列

此函数在移除重复项后返回给定序列的元素列表。此外,与 list(set(seq)) 不同,它会保留顺序。

from frappe.utils import unique

unique([1, 2, 3, 1, 1, 1]) # [1, 2, 3]
unique('abcda') # ['a', 'b', 'c', 'd']
unique(('Apple', 'Apple', 'Banana', 'Apple')) # ['Apple', 'Banana']

get_pdf(生成 PDF)

get_pdf(html, options=None, output=None)

`html`: HTML string to render
`options`: An optional `dict` for configuration
`output`: A optional `PdfFileWriter` object.

此函数使用 pdfkit 和 pyPDF2 模块从 HTML 生成 PDF 文件。如果提供了 output,则将生成的页面追加到此对象并返回它,否则返回 PDF 的 byte 流。

例如,生成 PDF 并将其作为对 Web 请求的响应返回:

import frappe
from frappe.utils.pdf import get_pdf

@frappe.whitelist(allow_guest=True)
def generate_invoice():
    cart = [{
        'Samsung Galaxy S20': 10,
        'iPhone 13': 80
    }]

    html = '
<h1>Invoice from Star Electronics e-Store!</h1>'

    # Add items to PDF HTML
    html += '
<ol>'
    for item, qty in cart.items():
        html += f'
<li>{item} - {qty}</li>'
    html += '</ol>'

    # Attaching PDF to response
    frappe.local.response.filename = 'invoice.pdf'
    frappe.local.response.filecontent = get_pdf(html)
    frappe.local.response.type = 'pdf'

get_abbr(获取缩写)

get_abbr(string, max_len=2)

返回给定 string 的缩写(仅首字母)版本,最多包含 max_len 个字母。它广泛用于 Frappe 框架和 ERPNext 中,以生成缩略图或占位图像。

from frappe.utils import get_abbr

get_abbr('Gavin') # 'G'
get_abbr('Coca Cola Company') # 'CC'
get_abbr('Mohammad Hussain Nagaria', max_len=3) # 'MHN'

validate_url(验证 URL)

validate_url(txt, throw=False, valid_schemes=None)

`txt`: A string to check validity
`throw`: Weather to throw an exception if `txt` does not represent a valid URL, `False` by default
`valid_schemes`: A string or an iterable (list, tuple or set). If provided, checks the given URL's scheme against this.

此实用函数可用于检查字符串是否表示有效的 URL 地址。

from frappe.utils import validate_url

validate_url('google') # False
validate_url('https://google.com') # True
validate_url('https://google.com', throw=True) # throws ValidationError

validate_email_address(验证电子邮件地址)

validate_email_address(email_str, throw=False)

返回一个字符串,其中包含给定 email_str 中存在的电子邮件地址或以逗号分隔的有效电子邮件地址列表。如果 throw 为 True,则在给定字符串中不存在有效电子邮件地址时抛出 frappe.InvalidEmailAddressError,否则返回空字符串。

from frappe.utils import validate_email_address

# Single valid email address
validate_email_address('[email protected]') # '[email protected]'
validate_email_address('other text, [email protected], some other text') # '[email protected]'

# Multiple valid email address
validate_email_address(
    'some text, [email protected], some other text, [email protected], yet another no-emailic phrase.'
) # '[email protected], [email protected]'

# Invalid email address
validate_email_address('some other text') # ''

validate_phone_number(验证电话号码)

validate_phone_number(phone_number, throw=False)

如果 phone_number(字符串)是有效的电话号码,则返回 True。如果 phone_number 无效且 throw 为 True,则会抛出 frappe.InvalidPhoneNumberError。

from frappe.utils import validate_phone_number

# Valid phone numbers
validate_phone_number('753858375') # True
validate_phone_number('+91-75385837') # True

# Invalid phone numbers
validate_phone_number('invalid') # False
validate_phone_number('87345%%', throw=True) # InvalidPhoneNumberError

frappe.cache()

cache()

返回 Redis 连接,该连接是 RedisWrapper 类的实例,继承自 redis.Redis 类。您可以使用此连接来利用 Redis 缓存存储/检索键值对。

import frappe

cache = frappe.cache()

cache.set('name', 'frappe') # True
cache.get('name') # b'frappe'

frappe.sendmail()

sendmail(recipients=[], sender="", subject="No Subject", message="No Message", as_markdown=False, template=None, args=None, **kwargs)

`recipients`: List of recipients
`sender`: Email sender. Default is current user or default outgoing account
`subject`: Email Subject
`message`: (or `content`) Email Content
`as_markdown`: Convert content markdown to HTML
`template`: Name of html template (jinja) from templates/emails folder
`args`: Arguments for rendering the template

在大多数情况下,上述参数已经足够,但此函数还可以接受许多其他关键字参数。要查看所有关键字参数,请查看此函数的实现(frappe/__init__.py)。

此函数可以使用用户的默认电子邮件账户或全局默认电子邮件账户发送邮件。

import frappe

recipients = [
    '[email protected]',
    '[email protected]'
]

frappe.sendmail(
    recipients=recipients,
    subject=frappe._('Birthday Reminder'),
    template='birthday_reminder',
    args=dict(
        reminder_text=reminder_text,
        birthday_persons=birthday_persons,
        message=message,
    ),
    header=_('Birthday Reminder 🎂')
)

示例 Jinja 模板文件:

<!-- templates/emails/birthday_reminder.html -->
<div>
<div class="gray-container text-center">
<div>
 {% for person in birthday_persons %}
 {% if person.image %}
 <img src="%7B%7B%20person.image%20%7D%7D" title="{{ person.name }}">
 {% endif %}
 {% endfor %}
 </div>
<div style="margin-top: 15px;"><span>{{ reminder_text }}</span>
<p class="text-muted">{{ message }}</p>
</div>
</div>

<h3 id="attaching-files">附加文件</h3>
<p>您可以通过向 <code>sendmail 函数传递附件列表来轻松地将文件附加到邮件中:

frappe.sendmail(
    ["[email protected]", "[email protected]"],
    message="## hello, *bro*"
    attachments=[{"file_url": "/files/hello.png"}],
    as_markdown=True
)

请注意,附件是一个字典列表,其中包含键 file_url。您可以在 File 文档的 file_url 字段中找到此 file_url。

文件锁

文件锁可用于同步进程,以避免竞争条件。

示例:如果多个写入者尝试写入同一个文件,可能会导致竞争条件。因此,我们创建一个命名锁,以便进程可以看到该锁并等待其可用于写入。

from frappe.utils.synchronization import filelock

def update_important_config(config, file):
    with filelock("config_name"):
        json.dumps(config, file)

get_filtered_list_url

get_filtered_list_url(doctype, docnames=None)

当创建了多个文档,并且您希望在列表视图中向用户展示所有这些文档时,此功能非常有用。

from frappe.utils import get_filtered_list_url

get_filtered_list_url("Work Order", [
    "MFG-WO-2025-00027",
    "MFG-WO-2025-00028",
    "MFG-WO-2025-00029"
])
# → 'http://
<site>/app/work-order?name=["in",["MFG-WO-2025-00027","MFG-WO-2025-00028","MFG-WO-2025-00029"]]'
</site>

get_filtered_list_link(doctype, docnames=None, label=None)

当创建了多个文档,并且您希望显示一个简洁的链接而不是冗长的消息对话框链接时,此功能非常有用。

from frappe.utils import get_filtered_list_link

get_filtered_list_link("Work Order", [
    "MFG-WO-2025-00027",
    "MFG-WO-2025-00028",
])
# → '<a href="http://<site>/app/work-order?name=%5B...%5D">Work Order</a>'

语言解析

在这里,让我们来看看 Frappe 中语言是如何解析的,以及你如何在你的
Frappe 应用或脚本中使用它们。

你会话的语言取决于 frappe.lang 的值。它的解析顺序如下:

  1. 表单字典 > _lang
  2. Cookie > preferred_language [仅限访客用户]
  3. 请求头 > Accept-Language [仅限访客用户]
  4. 用户文档 > language
  5. 系统设置 > language

表单字典:_lang

表单字典的 _lang 参数具有最高优先级。设置此参数将
更新给定请求中的所有可翻译组件。Frappe 在某些地方使用这种机制
来处理邮件模板和打印视图。

虽然在每个请求中传递 ?_lang=ru 可能不太实际。如果你
想要持久但临时的语言设置,你可以在 cookies 中设置
preferred_language 键。Frappe 利用这一点来实现网站
语言切换器。此方法可用于根据客户端来持久化语言设置。

仅对访客用户生效。对已登录用户忽略。

请求头:Accept-Language

另一种相对更简洁、更标准的管理语言的方法是使用
Accept-Language 请求头。如果前两种方法都未设置,Frappe 会开始
解析此请求头的值,其中包含客户端可接受语言的有序集合。你可以查看 Mozilla 文档
以获取更多相关信息。

仅对访客用户生效。对已登录用户忽略。

用户与系统设置

用户文档中有一个 language 字段,用于设置该用户的会话语言。
此设置会跨设备、客户端持久保存。这允许特定
用户以他们选择的语言查看网站和 Desk。例如,
如果一个用户在“法语”网站上将其语言设置为“俄语”,当他们
登录时,网站会自动翻译成俄语。

系统设置中的 language 字段用于设置整个
网站的语言。它的优先级最低,是所有会话的备用语言。

请求生命周期

Web 应用的用户可以访问不同的 URL,例如 /about、/posts 或 /api/resources。每个请求都根据以下请求类型进行处理。

  1. 以 /api 开头的 API 请求由 REST API 处理器处理。
  2. 文件下载,如备份文件 (/backups)、公共文件 (/files) 和私有文件 (/private/files),会单独处理,以响应可下载的文件。
  3. 网页请求,如 /about、/posts,由网站路由器处理。本页将进一步说明。

详细了解 API 请求和静态文件。

请求预处理

在触发路由规则之前,会发生一些操作。这些操作包括预处理请求、初始化记录器和速率限制器。

路径解析器

一旦请求从 app.py 到达网站路由器,它就会通过路径解析器进行处理。

路径解析器执行以下操作:

重定向解析

路径解析器尝试为传入的请求路径解析任何可能的重定向。路径解析器从 website_redirects 钩子获取重定向规则,并从网站设置中获取路由重定向。

路由解析

如果传入的请求路径没有重定向,路径解析器会尝试解析路由,以根据 website_routing_rules 钩子的规则以及启用了 has_web_view 的 DocType 文档中设置的动态路由来获取最终端点。

渲染器选择

一旦获得最终端点,它会传递给所有可用的页面渲染器,以检查哪个页面渲染器可以渲染给定的路径。第一个对 can_render 请求返回 True 的页面渲染器将用于渲染该路径。

页面渲染器

页面渲染器负责为给定的端点渲染页面或响应页面。页面渲染器使用 Python 类实现。一个页面渲染器类需要有两个方法,即 can_render 和 render。

路径解析器调用 can_render 来检查某个渲染器实例是否可以渲染特定路径。
一旦某个渲染器从 can_render 返回 True,该渲染器类就负责渲染该路径。

页面渲染器类示例

from frappe.website.page_renderers.base_renderer import BaseRenderer

class PageRenderer(BaseRenderer):
 def can_render(self):
 return True

 def render(self):
 response_html = "Response"
 return self.build_response(response_html)

以下是处理所有通用类型网页的标准页面渲染器。

StaticPage

使用 StaticPage,您可以从站点上安装的任何应用的 www 文件夹中提供 PDF、图片等文件。任何不是以下类型 html、md、js、xml、css、txt 或 py 的文件都被视为静态文件。
提供静态文件的首选方式是将它们添加到您的 frappe 应用的 public 文件夹中。这样,它们将由 NGINX 直接提供,利用压缩和缓存,同时减少延迟。

TemplatePage

TemplatePage 在所有应用中查找 www 文件夹,如果它是 HTML 或 Markdown 文件,则返回该文件;如果它是一个文件夹,则返回该文件夹中的 index.html 或 index.md 文件。

WebformPage

如果请求路径与任何可用的 Web 表单路由匹配,WebformPage 会尝试在 Web 表单列表中渲染 Web 表单。

DocumentPage

如果 DocType 的 /templates 文件夹中有可用的文档模板,DocumentPage 会尝试渲染该模板。模板文件名应与 DocType 名称相同。例如:如果您想为 User 文档类型添加文档模板,则 User DocType 的 templates 文件夹中应有 user.html。文件夹结构将类似于 doctype/user/templates/user.html

ListPage

如果 DocType 的 /templates 文件夹中有列表模板,ListPage 将渲染它。请查看 Blog Post 模板文件夹以获取实现参考。

PrintPage

PrintPage 渲染文档的打印视图。它使用标准打印格式,除非通过 default_print_format 为 DocType 设置了不同的打印格式。

NotFoundPage

NotFoundPage 渲染标准的未找到页面,并响应 404 状态码。

NotPermittedPage

NotPermittedPage 渲染标准的权限拒绝页面,并带有 403 状态码。

添加自定义页面渲染器

如果您有其他标准页面渲染器未处理的需求,可以通过 page_renderer [钩子] 添加自定义页面渲染器。

# in hooks.py of your custom app

page_renderer = "path.to.your.custom_page_renderer.CustomPage"

页面渲染器类需要有两个方法,即 can_render 和 render

路径解析器调用 can_render 来检查某个渲染器实例是否可以渲染特定路径。
一旦某个渲染器从 can_render 返回 True,该渲染器类就负责渲染该路径。

注意: 自定义页面渲染器具有优先权,其 can_render 方法将在标准页面渲染器之前被调用。

示例:


from frappe.website.utils import build_response
from frappe.website.page_renderers.base_renderer import BaseRenderer

class CustomPage(BaseRenderer):
 def can_render(self):
 return True

 def render(self):
 response_html = "Custom Response"
 return self.build_response(response_html)

注意:您还可以扩展标准页面渲染器,以覆盖或使用某些标准功能。

金贾API

这些是 frappe 提供的白名单方法,可用于 Jinja
模板中。

frappe.format

frappe.format(value, df, doc)

将原始值(存储在数据库中)格式化为用户可读的格式。
例如,将 2019-09-08 转换为 08-09-2019

用法

{{ frappe.format('2019-09-08', {'fieldtype': 'Date'}) }}

09-08-2019

frappe.format_date

frappe.format_date(date_string)

将日期格式化为人类可读的长格式。

用法

{{ frappe.format_date('2019-09-08') }}

September 8, 2019

frappe.get_url

frappe.get_url()

返回站点 URL

用法

{{ frappe.get_url() }}

https://frappe.io

frappe.get_doc

frappe.get_doc(doctype, name)

根据名称返回文档。

用法


 {% set doc = frappe.get_doc('Task', 'TASK00002') %}
 {{ doc.title }} - {{ doc.status }}

 Buy Eggs - Open

frappe.get_all

frappe.get_all(doctype, filters, fields, order_by, start, page_length, pluck)

返回某个 DocType 的所有记录列表。如果未提供 name 参数,则仅返回文档的 fields。

签名

frappe.get_all(doctype, filters, fields, order_by, start, page_length)

用法


 {% set tasks = frappe.get_all('Task', filters={'status': 'Open'}, fields=['title', 'due_date'], order_by='due_date asc') %}
 {% for task in tasks %}

### {{ task.title }}

Due Date: {{ frappe.format_date(task.due_date) }}

 {% endfor %}

### Redesign Website

Due Date: September 8, 2019

### Add meta tags on websites

Due Date: September 22, 2019

frappe.get_list

frappe.get_list(doctype, filters, fields, order_by, start, page_length)

与 frappe.get_all 类似,但会根据当前会话用户的权限过滤记录。

frappe.db.get_value

frappe.db.get_value(doctype, name, fieldname)

从文档中返回单个字段值(或值列表)。

用法



 {% set company_abbreviation = frappe.db.get_value('Company', 'TennisMart', 'abbr') %}
 {{ company_abbreviation }}

 {% set title, description = frappe.db.get_value('Task', 'TASK00002', ['title', 'description']) %}
 ### {{ title }}

{{ description }}

TM

frappe.db.get_single_value

frappe.db.get_single_value(doctype, fieldname)

从单个 DocType 返回字段值。

用法



 {% set timezone = frappe.db.get_single_value('System Settings', 'time_zone') %}
 {{ timezone }}

 Asia/Kolkata

frappe.get_system_settings

frappe.get_system_settings(fieldname)

从系统设置中返回字段值。

用法


 {% if frappe.get_system_settings('country') == 'India' %}
 Pay via Razorpay
 {% else %}
 Pay via PayPal
 {% endif %}

Pay via Razorpay

frappe.get_meta

frappe.get_meta(doctype)

返回文档类型的元数据。其中包含字段、标题字段、
图片字段等信息。

用法



 {% set meta = frappe.get_meta('Task') %}
 Task has {{ meta.fields | len }} fields.
 {% if meta.get_field('status') %}
 It also has a Status field.
 {% endif %}

 Task has 18 fields. It also has a Status field.

frappe.get_fullname

frappe.get_fullname(user_email)

返回传入用户邮箱的完整姓名。如果未传入用户,则假定为
当前登录用户。

用法


The fullname of [email protected] is {{ frappe.get_fullname('[email protected]') }}
The current logged in user is {{ frappe.get_fullname() }}

The fullname of [email protected] is Faris Ansari
The current logged in user is John Doe

frappe.render_template

frappe.render_template(template_name, context)

使用上下文渲染 Jinja 模板字符串或文件。

用法



 {{ frappe.render_template('templates/includes/footer/footer.html', {}) }}

{{ frappe.render_template('{{ foo }}', {'foo': 'bar'}) }}

bar

frappe._

frappe._(string) 或 _(string)

用法


{{ _('This string should get translated') }}

इस तार का अनुवाद होना चाहिए

frappe.session.user

返回当前会话用户

frappe.session.csrf_token

返回当前会话的 CSRF 令牌

frappe.form_dict

如果模板在 Web 请求中求值,则 frappe.form_dict 是
查询参数的字典,否则为 None。

frappe.lang

翻译函数当前使用的语言。两位小写字母代码。

数据库接口

版本 16 起,get_list 和 get_all 有一些破坏性变更。

frappe.db.get_list

frappe.db.get_list(doctype, filters, or_filters, fields, order_by, group_by, start, page_length, run)

  • 也可用别名 frappe.get_list 调用

从 doctype 表中返回记录列表。这是 SELECT 查询的 ORM 封装。它还会为当前会话用户应用记录的用户权限。如果未提供 fields 关键字参数,则仅返回文档名称。默认情况下,此方法返回一个 dict 列表,但您可以通过提供 pluck 关键字参数来提取特定字段:

frappe.db.get_list('Employee')

# output
[{'name': 'HR-EMP-00008'},
 {'name': 'HR-EMP-00006'},
 {'name': 'HR-EMP-00010'},
 {'name': 'HR-EMP-00005'}
]

# with pluck
frappe.db.get_list('Employee', pluck='name')

# output
['HR-EMP-00008',
 'HR-EMP-00006',
 'HR-EMP-00010',
 'HR-EMP-00005'
]

组合过滤器和其他参数:

frappe.db.get_list('Task',
    filters={
        'status': 'Open'
    },
    fields=['subject', 'date'],
    order_by='date desc',
    start=10,
    page_length=20,
    as_list=True
)

# output
(('Update Branding and Design', '2019-09-04'),
('Missing Documentation', '2019-09-02'),
('Fundraiser for Foundation', '2019-09-03'))

# Tasks with date after 2019-09-08
frappe.db.get_list('Task', filters={
    'date': ['>', '2019-09-08']
})

# Tasks with date between 2020-04-01 and 2021-03-31 (both inclusive)
frappe.db.get_list('Task', filters=[[
    'date', 'between', ['2020-04-01', '2021-03-31']
]])

# Tasks with subject that contains "test"
frappe.db.get_list('Task', filters={
    'subject': ['like', '%test%']
})

# Count number of tasks grouped by status
frappe.db.get_list('Task',
    fields=['count(name) as count', 'status'],
    group_by='status'
)

# For version 16 +
frappe.db.get_list('Task',
    fields=[{'COUNT': 'name', 'as': 'count'}, 'status'],
    group_by='status'
)

# output
[{'count': 1, 'status': 'Working'},
 {'count': 2, 'status': 'Overdue'},
 {'count': 2, 'status': 'Open'},
 {'count': 1, 'status': 'Filed'},
 {'count': 20, 'status': 'Completed'},
 {'count': 1, 'status': 'Cancelled'}]

在版本 16 之前,设置 run=False 会返回 SQL 查询而不是执行它。从版本 16 开始,您将获得一个查询构建器对象。您可以像以前一样调用 .get_sql() 来获取 SQL 查询,否则您可以在运行之前按需修改查询(query.run())。

frappe.db.get_all

frappe.db.get_all(doctype, filters, or_filters, fields, order_by, group_by, start, page_length, run)

  • 也可用别名 frappe.get_all 调用

与 frappe.db.get_list 相同,但会获取所有记录而不应用权限。

frappe.db.get_value

frappe.db.get_value(doctype, name, fieldname) 或 frappe.db.get_value(doctype, filters, fieldname)

  • 也可用别名 frappe.get_value 和 frappe.db.get_values 调用

返回文档的字段值或值列表。

# single value
subject = frappe.db.get_value('Task', 'TASK00002', 'subject')

# multiple values
subject, description = frappe.db.get_value('Task', 'TASK00002', ['subject', 'description'])

# as dict
task_dict = frappe.db.get_value('Task', 'TASK00002', ['subject', 'description'], as_dict=1)
task_dict.subject
task_dict.description

# with filters, will return the first record that matches filters
subject, description = frappe.db.get_value('Task', {'status': 'Open'}, ['subject', 'description'])

frappe.db.getsinglevalue

frappe.db.get_single_value(doctype, fieldname)

从单个 DocType 返回字段值。

timezone = frappe.db.get_single_value('System Settings', 'timezone')

frappe.db.set_value

frappe.db.set_value(doctype, name, fieldname, value)

  • 也可用别名 frappe.db.update 调用

在数据库中设置字段值,不会调用 ORM 触发器,但会更新修改时间戳(除非指定不更新)。

# update a field value
frappe.db.set_value('Task', 'TASK00002', 'subject', 'New Subject')

# update multiple values
frappe.db.set_value('Task', 'TASK00002', {
    'subject': 'New Subject',
    'description': 'New Description'
})

# update without updating the `modified` timestamp
frappe.db.set_value('Task', 'TASK00002', 'subject', 'New Subject', update_modified=False)

此方法不会调用 validate 和 on_update 等 ORM 触发器。使用此方法更新隐藏字段,或当您清楚自己在做什么时使用。

frappe.db.exists

frappe.db.exists(doctype, name)

如果文档记录存在,则返回 true。

传入 doctype 和 docname:

frappe.db.exists("User", "[email protected]", cache=True)

传入包含 "doctype" 键的过滤器字典:

frappe.db.exists({"doctype": "User", "full_name": "Jane Doe"})

传入 doctype 和过滤器字典:

frappe.db.exists("User", {"full_name": "Jane Doe"})

frappe.db.count

frappe.db.count(doctype, filters)

返回给定 doctype 和 filters 的记录数。

# total number of Task records
frappe.db.count('Task')

# total number of Open tasks
frappe.db.count('Task', {'status': 'Open'})

frappe.db.delete

frappe.db.delete(doctype, filters)

删除与 filters 匹配的 doctype 记录。这会执行 DML 命令,这意味着它可以回滚。如果未指定过滤器,则会删除该 doctype 的所有记录。

frappe.db.delete("Route History", {
    "modified": ("<=", last_record_to_keep[0].modified),
    "user": user
})

frappe.db.delete("Error Log")
frappe.db.delete("__Test Table")

您可以传入 doctype 名称或内部表名。按照惯例,Frappe 中的内部表以 __ 为前缀。API 遵循此约定。上述命令在表 tabError Log 和 __Test Table 上运行无条件的 DELETE 查询。

frappe.db.truncate

frappe.db.truncate(doctype)

截断数据库中的表。这会执行 DDL 命令 TRUNCATE TABLE,在执行语句之前会触发提交。此操作无法回滚。您可能希望使用它来定期清理日志表。

frappe.db.truncate("Error Log")
frappe.db.truncate("__Test Table")

上述命令在表 tabError Log 和 __Test Table 上运行 TRUNCATE 查询。

frappe.db.commit

frappe.db.commit()

提交当前事务。调用 SQL COMMIT。

在大多数情况下,您无需手动提交。请参阅下面的 Frappe 数据库事务模型。

frappe.db.savepoint

frappe.db.savepoint(save_point)

创建一个命名保存点,您可以稍后回滚到该点。

frappe.db.rollback

frappe.db.rollback()

回滚当前事务。调用 SQL ROLLBACK。

如果在类型为 POST 或 PUT 的 Web 请求期间抛出异常,Frappe 将自动运行 frappe.db.rollback()。如果您需要在事务中提前回滚,请使用此方法。

frappe.db.rollback(save_point="save_point_name")

回滚到特定保存点,而不是回滚整个事务。此回滚不会撤销对文件系统以及任何其他回滚监视器所做的更改。

frappe.db.sql

frappe.db.sql(query, values, as_dict)

执行任意 SQL 查询。这对于包含连接语句的复杂服务端报表、调整数据库以适应新功能等可能很有用。

示例:

values = {'company': 'Frappe Technologies Inc'}
data = frappe.db.sql("""
 SELECT
 acc.account_number
 gl.debit
 gl.credit
 FROM `tabGL Entry` gl
 LEFT JOIN `tabAccount` acc
 ON gl.account = acc.name
 WHERE gl.company = %(company)s
""", values=values, as_dict=0)

避免使用此方法,因为它会绕过验证和完整性检查。如果可能,最好使用 frappe.get_doc、frappe.db.get_list 等。

frappe.db.multisql

frappe.db.multisql({'mariadb': mariadb_query, 'postgres': postgres_query})

为任何受支持的数据库引擎执行合适的 SQL 语句。

frappe.db.rename_table

frappe.db.rename_table(old_name, new_name)

执行查询以更改表名。直接指定 DocType 或内部表名来重命名表。

示例:

frappe.db.rename_table("__internal_cache", "__temporary_cache")
frappe.db.rename_table("todo", "ToDo")

仅当您了解其影响时才应使用第二个示例。

不要使用此方法重命名 DocType 表。请改用 frappe.rename_doc 来实现。

frappe.db.describe

frappe.db.describe(doctype)

返回指定 DocType 的表结构描述元组。

frappe.db.changecolumntype

frappe.db.change_column_type(doctype, column, new_type)

更改指定 DocType 的列类型。

frappe.db.add_index

frappe.db.add_index(doctype, fields, index_name)

为指定字段的 DocType 创建索引。

注意:如果需要对 TEXT 或 BLOB 字段创建索引,必须指定固定长度。

示例:

frappe.db.add_index("Notes", ["id(10)", "content(500)"], index_name)

frappe.db.add_unique

frappe.db.add_unique(doctype, fields, constraint_name=None)

为指定字段的 DocType 创建唯一约束。

示例:

frappe.db.add_unique("DoctypeName",["field1","field2"])

frappe.db.bulk_update

frappe.db.bulk_update(doctype, doc_updates, *, chunk_size=100, modified=None, modified_by=None, update_modified=True, debug=False)

使用带有 CASE 表达式的单个 SQL UPDATE 语句,批量更新多个文档。

示例:

frappe.db.bulk_update(
    "Task",
    {
        "TASK-0001": {"status": "Closed", "description": "Completed by QA"},
        "TASK-0002": {"status": "Open", "description": "Pending assignment"},
    },
    chunk_size=200,
    modified_by="[email protected]",
    update_modified=True,
    debug=True,
)

doc_updates 格式:

{
    "DOC-0001": {"field1": "value1", "field2": "value2"},
    "DOC-0002": {"field1": "valueA", "field2": "valueB"},
}

注意事项:

  • 直接更新数据库;不会触发文档事件或验证。
  • 未指定的字段保持不变。

数据库事务钩子

注意:此 API 在 v15 版本中引入。

Frappe 提供了钩子,用于在发出提交(commit)或回滚(rollback)等事务命令之前或之后运行回调。这些钩子适用于以下场景:

  • 如果事务被回滚,则回滚数据库之外所做的更改。
  • 仅在事务提交后,才将数据库之外的更改刷新。

这些钩子包括:

  • frappe.db.before_commit.add(func: Callable)
  • frappe.db.after_commit.add(func: Callable)
  • frappe.db.before_rollback.add(func: Callable)
  • frappe.db.after_rollback.add(func: Callable)

用法示例:

def create_file(self):
    self.write_file()
    # This ensures rollback if DB transaction is rolledback
    frappe.db.after_rollback.add(self.rollback_file)

def rollback_file(self):
    self.delete_file()

数据库事务模型

Frappe 的数据库抽象层默认实现了一个合理的事务模型。因此,在大多数情况下,您无需手动处理 SQL 事务。该模型的简要描述如下:

Web 请求

  • 在执行 POST 或 PUT 时,如果对数据库进行了任何写入操作,这些操作将在请求成功结束时提交。
  • 使用 frappe.call 进行的 AJAX 调用默认是 POST,除非更改设置。
  • GET 请求不会触发隐式提交。
  • 请求处理期间发生的任何未捕获异常都将导致事务回滚。

后台/定时任务

  • 将函数作为后台或定时任务调用时,成功完成后将提交事务。
  • 任何未捕获异常都将导致事务回滚。

补丁(Patches)

  • 补丁的 execute 函数成功完成后,将自动提交事务。
  • 任何未捕获异常都将导致事务回滚。

单元测试

  • 运行一个测试模块后,事务将被提交。测试模块指的是任何 Python 测试文件,例如 test_core.py。
  • 所有测试完成后,事务也会被提交。
  • 任何未捕获异常都将退出测试运行器,因此不会提交事务。

注意:如果您在任何地方捕获了异常,数据库抽象层将无法感知到错误的发生,因此您需要负责正确回滚事务。

文档接口

文档是 DocType 的一个实例。它派生自 frappe.model.Document 类,并代表数据库表中的一条记录。

frappe.get_doc

frappe.get_doc(doctype, name)

返回由 doctype 和 name 标识的记录对应的文档对象。如果未找到文档,则会引发 DoesNotExistError 异常。如果 doctype 是单例 DocType,则不需要 name。

# get an existing document
doc = frappe.get_doc('Task', 'TASK00002')
doc.title = 'Test'
doc.save()

# get a single doctype
doc = frappe.get_doc('System Settings')
doc.timezone # Asia/Kolkata

frappe.get_doc(dict)

返回一个内存中新的文档对象,该对象在数据库中尚不存在。

# create a new document
doc = frappe.get_doc({
    'doctype': 'Task',
    'title': 'New Task'
})
doc.insert()

frappe.get_doc(doctype={document_type}, key1 = value1, key2 = value2, ...)

返回一个内存中新的文档对象,该对象在数据库中尚不存在。

# create new object with keyword arguments
user = frappe.get_doc(doctype='User', email_id='[email protected]')
user.insert()

frappe.get_last_doc

frappe.get_last_doc(doctype, filters, order_by)

返回在指定 doctype 下创建的最后一个文档对象。

# get the last Task created
task = frappe.get_last_doc('Task')

您还可以指定过滤器来优化结果。例如,您可以通过添加过滤器来检索最后一个已取消的任务。

# get the last available Cancelled Task
task = frappe.get_last_doc('Task', filters={"status": "Cancelled"})

默认情况下,order_by 参数设置为 creation desc,但可以覆盖此值以使用其他可以达到相同目的的非标准字段。例如,您在 任务 DocType 下有一个字段 timestamp,它记录的是任务被批准或标记为有效的时间,而不是创建时间。

# get the last Task created based on a non-standard field
task = frappe.get_last_doc('Task', filters={"Status": "Cancelled"}, order_by="timestamp desc")

或者,您也可以完全反其道而行之,作为玩笑将其更改为“creation asc”来检索第一个文档。

frappe.get_cached_doc

类似于 frappe.get_doc,但会先查询缓存中的文档,然后再访问数据库。

frappe.new_doc

frappe.new_doc(doctype)

创建新文档的另一种方式。

# create a new document
doc = frappe.new_doc('Task')
doc.title = 'New Task 2'
doc.insert()

frappe.delete_doc

frappe.delete_doc(doctype, name)

从数据库中删除该记录及其子记录。同时也会删除与之关联的其他文档,如通信、评论等。

frappe.delete_doc('Task', 'TASK00002')

frappe.rename_doc

frappe.rename_doc(doctype, old_name, new_name, merge=False)

将文档的 name(主键)从 old_name 重命名为 new_name。如果 merge 为 True 且存在 new_name 的记录,则会将该记录与其合并。

frappe.rename_doc('Task', 'TASK00002', 'TASK00003')

只有在 DocType 表单中设置了 允许重命名 时,重命名操作才会生效。

frappe.get_meta

frappe.get_meta(doctype)

返回 doctype 的元信息。这也会应用自定义字段和属性设置器。

meta = frappe.get_meta('Task')
meta.has_field('status') # True
meta.get_custom_fields() # [field1, field2, ..]

要获取 DocType 的原始文档(不含自定义字段和属性设置器),请使用 frappe.get_doc('DocType', doctype_name)。

frappe.only_for

frappe.only_for(roles, message=False)

如果当前用户没有任何允许的角色,则引发 frappe.PermissionError 异常。

如果当前用户是 Administrator,则跳过权限检查。

# restrict action to System Manager role
frappe.only_for("System Manager")

您也可以允许多个角色:

# allow multiple roles
frappe.only_for(["System Manager", "Accounts Manager"])

frappe.get_docs

frappe.get_docs(doctype, filters, *, chunk_size=1000, limit=None, limit_start=0, order_by="creation asc", as_iterator=False)

返回文档对象列表。使用 as_iterator=True 分块获取记录,以便更好地管理内存。

# Fetch specific documents with child tables
tasks = frappe.get_docs('Task', filters={'status': 'Open'}, limit=10)

for task in tasks:
    task.status = "Closed"
    task.save()

# Efficiently iterate through large datasets
leads = frappe.get_docs('Lead', as_iterator=True, chunk_size=500)

for lead in leads:
    lead.process_lead() # Custom controller method

文档方法

本节列出了 doc 对象上可用的常用方法。

doc.insert

此方法将新文档插入数据库表。它会检查用户权限,如果控制器中编写了 before_insert、validate、on_update、after_insert 方法,则会执行这些方法。

它有一些“逃生舱”机制,可用于跳过下面说明的某些检查。

doc.insert(
    ignore_permissions=True, # ignore write permissions during insert
    ignore_links=True, # ignore Link validation in the document
    ignore_if_duplicate=True, # dont insert if DuplicateEntryError is thrown
    ignore_mandatory=True # insert even if mandatory fields are not set
)

doc.save

此方法保存对现有文档的更改。它会检查用户权限,并在更新前执行 validate,在更新值后执行 on_update。

doc.save(
    ignore_permissions=True, # ignore write permissions during insert
    ignore_version=True # do not create a version record
)

doc.delete

从数据库表中删除文档记录。此方法是 frappe.delete_doc 的别名。

doc.delete()

doc.get_doc_before_save

返回更改前的文档版本。您可以使用它来比较自上次版本以来发生了什么变化。

old_doc = doc.get_doc_before_save()
if old_doc.price != doc.price:
    # price changed
    pass

doc.has_value_changed

如果给定字段的值在保存前后发生了变化,则返回 True。

price_changed = doc.has_value_changed("price")

if price_changed:
    pass

doc.reload

将从数据库获取最新值并更新文档状态。

当您在处理文档时,可能代码的其他部分会直接更新数据库中某个字段的值。在这种情况下,您可以使用此方法重新加载文档。

doc.reload()

doc.check_permission

如果当前用户没有所提供权限类型的权限,则抛出异常。

doc.check_permission(permtype='write') # throws if no write permission

doc.get_title

根据 title_field 或名为 title 或 name 的字段获取文档标题。

title = doc.get_title()

doc.notify_update

发布实时事件以指示文档已被修改。客户端事件处理程序通过更新表单来响应此事件。

doc.notify_update()

doc.db_set

直接在数据库中设置文档的字段值,并更新修改时间戳。

此方法不会触发控制器验证,应谨慎使用。

# updates value in database, updates the modified timestamp
doc.db_set('price', 2300)

# updates value in database, will trigger doc.notify_update()
doc.db_set('price', 2300, notify=True)

# updates value in database, will also run frappe.db.commit()
doc.db_set('price', 2300, commit=True)

# updates value in database, does not update the modified timestamp
doc.db_set('price', 2300, update_modified=False)

doc.append

向子表追加一个新项目。

doc.append("childtable", {
    "child_table_field": "value",
    "child_table_int_field": 0,
    ...
})

doc.get_url

返回此文档的 Desk URL。例如:/app/task/TASK00002

url = doc.get_url()

doc.add_comment

向此文档添加评论。评论将显示在表单视图的时间线中。

# add a simple comment
doc.add_comment('Comment', text='Test Comment')

# add a comment of type Edit
doc.add_comment('Edit', 'Values changed')

# add a comment of type Shared
doc.add_comment("Shared", "{0} shared this document with everyone".format(user))

doc.add_seen

将给定/当前用户添加到已查看此文档的用户列表中。这将更新表中的 _seen 列。该列以 JSON 数组形式存储。

# add john to list of seen
doc.add_seen('[email protected]')

# add session user to list of seen
doc.add_seen()

此功能仅在 DocType 中启用了 跟踪已查看 时才有效。

doc.add_viewed

当用户查看文档(即打开表单)时,添加一条查看日志。

# add a view log by john
doc.add_viewed('[email protected]')

# add a view log by session user
doc.add_viewed()

此功能仅在 DocType 中启用了 跟踪查看 时才有效。

doc.add_tag

向文档添加标签。标签通常用于筛选和分组文档。

# add tags
doc.add_tag('developer')
doc.add_tag('frontend')

doc.get_tags

返回与特定文档关联的标签列表。

# get all tags
doc.get_tags()

doc.run_method

如果控制器中定义了方法,则运行该方法;如果定义了钩子,也会触发钩子。

doc.run_method('validate')

doc.queue_action

在后台运行控制器方法。如果该方法具有内部函数,例如 _submit 对应 submit,则将调用该内部函数。

doc.queue_action('send_emails', emails=email_list, message='Howdy')

doc.get_children()

仅适用于树形 DocType(继承自 NestedSet)。

返回一个生成器,为每个子记录生成一个 NestedSet 实例。

for child_doc in doc.get_children():
    print(child_doc.name)

它也可以递归应用:

for child_doc in doc.get_children():
    print(child_doc.name)
    for grandchild_doc in child_doc.get_children():
        print(grandchild_doc.name)

doc.get_parent()

仅适用于树形 DocType(继承自 NestedSet)。

返回父记录的 NestedSet 实例。

parent_doc = doc.get_parent()
grandparent_doc = parent_doc.get_parent()

doc.db_insert()

将文档序列化并插入数据库。警告:此操作会绕过所有验证以及插入前后可能需要运行的控制器方法。如有疑问,请改用 doc.insert()。

doc = frappe.get_doc(doctype="Controller", data="")
doc.db_insert()

doc.db_update()

将文档序列化并更新到数据库。警告:此操作会绕过所有验证以及更新前后可能需要运行的控制器方法。如有疑问,请改用 doc.save()。

doc = frappe.get_last_doc("User")
doc.last_active = now()
doc.db_update()

后台作业

Frappe 内置了一套在后台运行任务的系统。它通过使用 schedule 包和一个简单的长时间运行的无限 while 循环来实现。

你可以使用 frappe.enqueue 方法将一个 Python 方法加入队列以在后台运行:

def long_running_job(param1, param2):
    # expensive tasks
    pass

# directly pass the function
frappe.enqueue(long_running_job, queue='short', param1='A', param2='B')

# or pass the full module path as string
frappe.enqueue('app.module.folder.long_running_job', queue='short', param1='A', param2='B')

以下是你传递给 enqueue 的所有可能参数:

frappe.enqueue(
    method, # python function or a module path as string
    queue="default", # one of short, default, long
    timeout=None, # pass timeout manually
    is_async=True, # if this is True, method is run in worker
    now=False, # if this is True, method is run directly (not in a worker) 
    job_name=None, # specify a job name
    enqueue_after_commit=False, # enqueue the job after the database commit is done at the end of the request
    at_front=False, # put the job at the front of the queue
    **kwargs, # kwargs are passed to the method as arguments
)

你也可以使用 frappe.enqueue_doc 将一个文档(Document)方法加入队列:

frappe.enqueue_doc(
    doctype,
    name,
    "do_something", # name of the controller method
    queue="long",
    timeout=4000,
    param="value"
)

队列

框架默认配置了 3 个队列:short、default 和 long。每个队列都有一个默认超时时间,如下所示:

  • short:300 秒
  • default:300 秒
  • long:1500 秒

你也可以向 enqueue 方法传递自定义的超时时间。

自定义队列

你可以通过在 [common_site_config.json](https://frappeframework.com/docs/v14/user/en/basics/site_config#common-site-config) 中进行配置来添加自定义队列:

{
    ...
    "workers": {
        "myqueue": {
            "timeout": 5000, # queue timeout
            "background_workers": 4, # number of workers for this queue
        }   
    }
}

工作进程

默认情况下,Frappe 会设置 3 种工作进程类型来消费各个队列中的任务。默认配置如下所示:

bench worker --queue short
bench worker --queue default
bench worker --queue long

在生产环境中,这 3 个工作进程会被复制到配置数量的后台工作进程中,以处理更高的工作负载。

注意:这种将工作进程映射到单个队列的方式只是一种约定,并非必须遵循。

多队列消费

你可以通过指定一个逗号分隔的队列名称字符串,来让工作进程消费多个队列。

示例:如果你想要合并 short 和 default 工作进程,并且只使用两种类型的工作进程而不是默认配置,你可以像这样修改你的工作进程配置:

bench worker --queue short,default
bench worker --queue long

注意:这里展示的示例是针对 Procfile 格式的,但它们也很容易应用于 supervisor 或 systemd 配置。

使用 --burst 的突发模式

bench worker --queue short --burst

该命令会生成一个临时工作进程,该进程将开始消费 short 队列,并在队列清空后退出。如果你定期需要更多的工作进程,你可以使用操作系统的 crontab 在特定时间设置突发工作进程。

调度器事件

你可以使用调度器事件,通过 scheduler_events 钩子在后台定期运行任务。

app/hooks.py

scheduler_events = {
    "hourly": [
        # will run hourly
        "app.scheduled_tasks.update_database_usage"
    ],
}

app/scheduled_tasks.py

def update_database_usage():
    pass

在 hooks.py 中更改任何计划事件后,你需要运行 bench migrate 才能使更改生效。

可用事件

  • hourly、daily、weekly 和 monthly

这些事件将分别每小时、每天、每周和每月触发一次。

  • hourly_long、daily_long、weekly_long、monthly_long

与上述相同,但这些任务在 long 工作进程中运行,适用于长时间运行的任务。

  • all

all 事件每 4 分钟触发一次。这可以通过 common_site_config.json 中的 scheduler_interval 键进行配置。

  • cron

一个有效的 cron 字符串,可以被 croniter 解析。

使用示例:

scheduler_events = {
    "daily": [
        "app.scheduled_tasks.manage_recurring_invoices"
    ],
    "daily_long": [
        "app.scheduled_tasks.take_backups_daily"
    ],
    "cron": {
        "15 18 * * *": [
            "app.scheduled_tasks.delete_all_barcodes_for_users"
        ],
        "*/6 * * * *": [
            "app.scheduled_tasks.collect_error_snapshots"
        ],
        "annual": [
            "app.scheduled_tasks.collect_error_snapshots"
        ]
    }
}

可配置的调度器事件

在需要用户可配置触发间隔的场景下,创建一个 Scheduler Event 记录,并针对该记录创建一个 Scheduled Job Type 条目。这不需要 scheduler_event 钩子。

示例:

# Create `Scheduler Event` record
sch_eve = frappe.new_doc("Scheduler Event")
sch_eve.scheduled_against = "Process Payment Reconciliation"
sch_eve.save()

# Create `Scheduled Job Type`
job = frappe.new_doc("Scheduled Job Type")
job.frequency = "Cron"
job.scheduler_event = sch_eve.name
job.cron_format = "0/5 * * * *"     # runs every five minutes
job.save()

Scheduled Job Type 的触发间隔可以在之后修改,并且会在 bench 迁移时保持不变。

由调度器触发的任务由 管理员 用户运行。这也意味着,除非另有指定,否则你通过计划任务创建的任何文档都将归 管理员 用户所有。