数据库接口

版本 16 起,get_listget_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_valuefrappe.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)

此方法不会调用 validateon_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)

返回给定 doctypefilters 的记录数。

# 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

如果在类型为 POSTPUT 的 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 请求

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

后台/定时任务

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

补丁(Patches)

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

单元测试

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

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

文档接口

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

frappe.get_doc

frappe.get_doc(doctype, name)

返回由 doctypename 标识的记录对应的文档对象。如果未找到文档,则会引发 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。如果 mergeTrue 且存在 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_insertvalidateon_updateafter_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 或名为 titlename 的字段获取文档标题。

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 个队列:shortdefaultlong。每个队列都有一个默认超时时间,如下所示:

  • 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 才能使更改生效。

可用事件

  • hourlydailyweeklymonthly

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

  • hourly_longdaily_longweekly_longmonthly_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 迁移时保持不变。

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

实时(socket.io)

Frappe 内置了一个基于 socket.io 的实时事件 API。由于 socket.io 需要 Node 服务器来运行,我们在主 Web 服务器之外并行运行一个 Node 进程。

客户端 API (JavaScript)

frappe.realtime.on

要在客户端(浏览器)监听实时事件,可以使用 frappe.realtime.on 方法:

frappe.realtime.on('event_name', (data) => {
    console.log(data)
})

frappe.realtime.off

停止监听您已订阅的事件:

frappe.realtime.off('event_name')

服务端 API (Python)

frappe.publish_realtime

要从服务端发布实时事件,可以使用 frappe.publish_realtime 方法:

frappe.publish_realtime('event_name', data={'key': 'value'})

frappe.publish_progress

您可以使用此方法在对话框中显示进度条:

frappe.publish_progress(25, title='Some title', description='Some description')

自定义事件处理器 (Python)

注意:此功能仅在夜间版中可用。此功能被视为实验性功能。

您可以通过在自定义应用中创建 your_app/your_app/realtime/handlers.py 文件来实现自定义的实时事件处理器。其语法与 API 白名单非常相似。

from frappe.realtime import realtime

@realtime.on(
  "project_subscribe",
  frappe_context=False,   # open a Frappe context (DB + session) for the handler body
  allow_guest=False,      # if False, the event is dropped when socket.user == "Guest"
)

默认值:frappe_context=Falseallow_guest=False

示例

import frappe
from frappe.realtime import Socket, realtime

@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
    if socket.has_permission("Project", project):
        socket.join(f"project:{project}")

第一个参数始终是输入的 Socket。其余参数是客户端发送的负载,按位置传递。事件名称 "project_subscribe" 是浏览器通过 frappe.realtime.emit(...) 发出的。

使用 frappe_context=True 的工作示例

使用 frappe_context=True 时,完整的 ORM 在处理器主体中可用 — frappe.get_docfrappe.local.sitefrappe.session.user 都会被填充,并反映已认证的 socket 用户。

此示例处理器加载一个文档,检查权限,并将结果发送回客户端:

@realtime.on("test_get_doc", frappe_context=True)
def test_get_doc(socket: Socket, doctype: str, docname: str) -> None:
    import frappe
    try:
        doc = frappe.get_doc(doctype, docname)
        doc.check_permission()
        socket.emit(
            "test_get_doc_result",
            {
                "ok": True,
                "site": frappe.local.site,
                "user": frappe.session.user,
                "doctype": doc.doctype,
                "name": doc.name,
                "modified": str(doc.get("modified")),
            },
        )
    except Exception as e:
        socket.emit("test_get_doc_result", {"ok": False, "error": f"{type(e).__name__}: {e}"})

从浏览器端:

frappe.realtime.on("test_get_doc_result", (r) => console.log(r));
frappe.realtime.emit("test_get_doc", "User", "Administrator");

请参阅“权限检查”部分,了解其成本以及何时应优先使用基于 HTTP 的廉价权限检查。

仅当处理器所属的应用安装在连接站点上时,该处理器才会运行。所属应用会自动从导入中检测出来。

您可能需要重启 socketio 服务器才能看到代码更改生效。有关编写自定义事件处理器的更多信息,请参阅 Socket.IO 文档。

Socket 对象

只读身份信息,在连接时填充:

socket.site            # the site this socket is on
socket.user            # "Guest" for anonymous
socket.user_type       # e.g. "System User"
socket.installed_apps  # list of installed apps

房间、发送和瞬态状态:

socket.join(room)                        # add this socket to a room
socket.leave(room)                       # remove it
socket.emit(event, data=None, room=None) # emit to a room, or to this client if room is None
socket.get(key, default=None)            # read transient per-socket state
socket.set(key, value)                   # persist transient per-socket state (cleared on disconnect)

权限检查

有两种方法可以执行 doctype 权限检查。

A. HTTP(默认,推荐)socket.has_permission(doctype, name) 向 Web 进程发起请求,与核心处理器的行为完全一致。实时进程中不建立数据库连接。成本低。除非有特殊原因,否则请使用此方法。

if socket.has_permission("Project", project):
    socket.join(f"project:{project}")

B. 进程内(frappe_context=True:为处理器主体打开一个完整的 Frappe 上下文,以便您可以直接调用 frappe.has_permission(...)、查询数据库等。代价:每个此类事件都会执行 frappe.init -> connect -> set_user -> commit/rollback -> destroy,并强制在实时进程中建立数据库连接。请谨慎使用。

@realtime.on("project_subscribe", frappe_context=True)
def project_subscribe(socket: Socket, project: str) -> None:
    if frappe.has_permission("Project", doc=project, ptype="read"):
        socket.join(f"project:{project}")

完整参考:++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++

自定义事件处理器 (NodeJS)

从 v16 版本开始可用。 被视为实验性功能。这是之前的实现,将在新版本中逐步淘汰。

Socket.IO 服务器路径;新应用应优先使用上述 Python 处理器。

您可以通过在应用的 realtime 文件夹中创建 handlers.js 文件来实现自定义的实时事件处理器。

此文件需要有一个单一的导出 — 一个在 socket 实例上设置事件处理器的函数。例如,对于一个名为“chat”的应用:

// bench/apps/chat/realtime/handlers.js

function chat_app_handlers(socket) {
    socket.on("hello_chat", () => {
        console.log("hello world!");
    });
}

module.exports = chat_app_handlers;

使用 frappe.realtime.emit("hello_chat") 从客户端代码触发此事件。您可能需要重启 socketio 服务器才能看到代码更改生效。有关编写自定义事件处理器的更多信息,请参阅 Socket.IO 文档。

向客户端推送事件

您不能直接从 Web 进程发送事件 — 您需要通过 Redis 发布,然后实时服务器将其桥接到已连接的 socket。您发布到的房间字符串必须与您的处理器 join 到的房间匹配。

from frappe.realtime import publish_to_room
publish_to_room("project:PROJ-0001", "project_updated", {"status": "Open"})

其他命名的辅助函数,每个都是对 publish_realtime 的轻量封装。

publish_to_user(user, event, message=None)
publish_to_doc(doctype, docname, event, message=None)
publish_to_doctype(doctype, event, message=None)
publish_task_progress(task_id, message=None)
publish_to_website(event, message=None)
publish_to_all(event, message=None)
publish_to_room(room, event, message=None)

完整参考:++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++

自定义客户端

如果你正在开发一个不使用 Desk 界面的 SPA 或移动应用,你可以编写自定义客户端来连接 socket.io 服务器。请参考官方 Socket.IO 客户端文档。

自定义客户端示例:

  • gameplan/frontend/src/socket.js
  • frappe/socketio_client.js

自定义客户端中的授权

有两种方式可以对 socket.io 服务器的连接进行身份验证:

  • Cookies — 在类似浏览器的环境中,连接会自动发送 cookies,socketio 服务器会使用它们进行身份验证。
  • 授权头 — 如果 cookies 不可用(例如移动应用),请像 API 请求一样使用 Authorization 头。请参阅 REST API 身份验证文档和 Socket.IO extraHeaders。

实现说明

  • 实时服务器使用 socket.io 服务器,用 node.js 编写,位于 /realtime 目录中。
  • 实时客户端是 socket.io 客户端库的封装,位于 public/js/frappe/socketio_client.js 中。
  • Python 进程通过 Redis 发布-订阅通道将事件发布到 node 服务器。实时服务器订阅 Redis 通道,并重新发布给所有已订阅的客户端。
  • 实时服务器是多租户的:所有站点流量都按站点名称进行命名空间隔离。命名空间会动态创建为 /{sitename},其中 sitename 是站点在 sites 目录中的文件夹名称(或 frappe.local.site)。
  • 实时服务器使用主 Frappe Web 服务器来验证连接。SID cookie 或授权头会传递给客户端,并用于确保连接是有效用户,且可以根据权限订阅 DocTypes/文档。

可用房间

房间 访问权限
all 所有系统用户默认连接
website 任何用户(包括访客)均可访问
user:{username} 按用户分配的房间。无需权限检查即可加入
doctype:{doctype} 按 DocType 分配的房间。只有具有 DocType 权限的用户才能加入;打开列表/表单视图时自动订阅
doc:{doctype}/{name} 按文档分配的房间。只有具有文档权限的用户才能加入;打开表单视图时自动订阅

提交时允许

提交后允许编辑

提交后允许编辑 是一个 DocField 属性,它允许字段在文档提交后仍可编辑。已提交的文档通常会被锁定以保护审计跟踪。仅当字段可以安全更改,且不会改变已提交文档的含义、价值、状态或分类账影响时,才应使用此选项。

何时使用“提交后允许编辑”

为那些在提交后记录附加信息,但不影响验证、总计、库存、会计、工作流决策或外部承诺的字段启用此属性。

  • 提交后收到的参考编号
  • 备注、说明或内部评论
  • 跟踪详情或发货信息
  • 仅用于打印的标签或操作元数据

何时不应使用

不要为影响业务逻辑或已提交文档的财务、库存或合规含义的字段启用“提交后允许编辑”。如果更改某个字段应改变交易,则应取消并修改文档,而不是直接编辑。

  • 金额、数量、费率、税费、折扣或总计
  • 往来单位、公司、过账日期、仓库、项目、科目或货币字段
  • 用于工作流、集成、报告或验证的状态字段
  • 任何用于创建分类账分录或库存分类账分录的字段

使用“自定义表单”启用“提交后允许编辑”

  1. 转到 自定义表单
  2. 选择需要在提交后保持可编辑状态的 DocType。

表单构建器将打开并显示所选 DocType 的字段。在此示例中,选择了 待办事项 DocType。

  1. 在字段表中,选择提交后应保持可编辑状态的字段。
  2. 在字段属性中,启用 提交后允许编辑
  3. 点击 更新 以保存自定义设置。

使用属性搜索可以快速找到 提交后允许编辑,尤其是在字段具有许多属性时。

验证行为

  1. 打开您自定义的 DocType 的一个已提交文档。
  2. 确认只有预期的字段可编辑。
  3. 更改字段值并保存文档。
  4. 检查总计、分类账分录、工作流状态以及其他已提交的值是否未发生变化。

在启用“提交后允许编辑”之前,请检查该字段是否在控制器方法、自定义脚本、报告、打印格式、工作流、集成或权限逻辑中使用。如果该字段在任何地方用于计算或决定重要事项,请在提交后保持其锁定状态。

对于自定义应用程序,还应审查服务器端验证。允许在已提交的文档上编辑字段并不会自动使所有相关的业务规则变得安全。请在需要的地方添加验证,以便只允许预期的提交后更新。

故障排除

启用“提交后允许编辑”后,字段仍为只读

重新加载表单,并检查该字段是否也受只读依赖项、角色权限、工作流状态、自定义脚本或服务器端逻辑的控制。

表单上的值已更改,但无法保存

检查该 DocType 的自定义验证和控制器钩子。服务器端验证可能仍会阻止对已提交文档的更新。

该字段应影响交易

不要使用“提交后允许编辑”。请取消并修改已提交的文档,以便更正后的交易具有清晰的审计跟踪。

单据类型布局

文档类型布局

文档类型布局允许您为同一文档类型创建多个表单视图——每个视图可以有不同的字段标签、可见性规则和默认值——而无需修改文档类型本身。布局会根据文档状态自动激活,因此无需手动操作即可显示正确的视图。

常见用例

  • 隐藏高级字段的简化数据录入视图
  • 重新标记字段以提高清晰度的侧重阅读的审阅视图
  • doc.is_return == 1 时自动切换到“退货”布局
  • 由条件或工作区链接驱动的状态或上下文特定布局

创建布局

转到文档类型布局列表,然后点击新建

字段 描述
标题 在布局指示器和面包屑中显示的显示名称(例如 紧凑视图)。
文档类型 此布局适用的文档类型。设置后无法更改。
条件 可选的 JS 表达式。当表达式为真时,此布局会在表单刷新时自动激活。
基于 从同一文档类型的另一个布局继承字段配置。
是子表 将此布局标记为子表布局。选中后,它将出现在其他文档类型布局的子布局选择器中。
默认打印格式 当此布局激活时,预选此打印格式。
默认电子邮件模板 当此布局激活时,预选此电子邮件模板。

选择文档类型后,点击同步字段以拉取文档类型中的所有字段。一个可视化表单构建器将在父布局选项卡中打开,您可以在其中重新排序和配置字段。

覆盖字段属性

在表单构建器中,选择任意字段以覆盖其属性。以下属性可以按布局设置,而不会影响基础文档类型:

属性 效果
label 为此布局重命名字段
hidden 隐藏字段
reqd 将字段设为必填
read_only 将字段设为只读
bold 强调标签
allow_in_quick_entry 在快速录入对话框中包含此字段
in_list_view 在列表视图中将字段显示为列
in_standard_filter 在标准筛选器栏中包含此字段
default 为新文档设置默认值
description 覆盖帮助文本
depends_on 条件可见性表达式
mandatory_depends_on 条件必填表达式
read_only_depends_on 条件只读表达式

布局中未设置的属性将回退到基础文档类型的定义。

自动布局切换

布局选择完全由条件驱动——没有手动切换器。设置一个条件,使布局在表单刷新后表达式评估为真时自动激活:

// Activates for return transactions
doc.is_return == 1

// Activates for high-value orders
doc.grand_total > 100000

// Activates for a specific status
doc.status === "Closed"

当条件匹配时,布局将被应用,并且 URL 会更新为 ?layout=<name></name>。第一个匹配的布局优先;布局按其在列表中出现的顺序进行评估。每当非默认布局激活时,文档标题旁边会出现一个布局指示器徽章。

当布局激活时,面包屑栏会反映出来:

  • 表单视图中,布局标题作为可点击的面包屑显示在文档类型和文档名称之间。点击它会打开按布局条件过滤的列表(从 doc.field OP value 表达式解析)。
  • 在该过滤列表上,布局标题显示为不可点击的面包屑,以便活动上下文保持可见。
  • 当布局激活时,点击文档类型面包屑会清除所有筛选器并移除布局面包屑,返回到未过滤的列表。

子表布局

布局可以为其子表指定不同的布局。在子布局选项卡中,为您要自定义的每个子表添加一行:

字段 描述
表字段名 父文档类型中子表的字段名
子布局 一种文档类型布局,其文档类型与子表的文档类型匹配

当父布局处于活动状态时,每个子表网格会自动使用其指定的子布局进行渲染——应用与父布局相同的字段级覆盖。

重要提示。 子布局的文档类型必须与子表的文档类型匹配(例如 Sales Invoice Item),而不是父文档类型。请先为子表创建单独的布局,然后在此处引用它。

父布局上可用的相同字段属性——labelhiddenreqdread_onlydepends_on、列顺序等——在子布局激活时同样适用于网格内部。

示例。 一个财务视图父布局将项目表映射到紧凑项目子布局,该子布局隐藏了 descriptionuom 和其他明细列,仅保留 item_codeqtyamount 可见。切换到父布局会自动压缩网格——用户无需额外操作。

布局继承

使用基于来继承同一文档类型的另一个布局的字段配置。子布局以父布局的字段顺序和覆盖为起点,并可以进一步自定义单个字段或添加新字段。

这对于创建一系列相关布局非常有用:先定义一个包含通用字段顺序的基础布局,然后创建继承并调整它的专门布局(例如销售视图财务视图)。

保持字段同步

同步字段按钮会将布局的字段列表与当前文档类型定义进行比较,并报告:

  • 已添加 — 文档类型中存在但布局中缺少的字段(添加到末尾)
  • 已移除 — 布局中存在但文档类型中已不再存在的字段(已删除)

在修改底层文档类型后运行此操作,以保持布局的一致性。

工作区集成

侧边栏工作区中的任何文档类型链接都可以绑定到特定布局。以编辑模式打开工作区,选择一个文档类型链接,然后将文档类型布局字段设置为您想要的布局。点击该链接会打开预先限定为该布局的列表(URL 携带 ?layout=<name></name>),并且从列表创建的新文档会在该布局下打开。

面向开发者:标准布局

通过设置为标准并选择模块,布局可以作为应用的一部分发布。在开发者模式下,保存标准布局会自动将其导出为 JSON 文件到:

{app}/{module}/doctype/{doctype}/doctype_layout/{layout_name}.json

该文件包含在迁移中,并通过 bench migrate 自动部署。标准布局在生产实例(开发者模式之外)上是只读的。

权限类型

注意:此功能自 v16 版本起可用。

自定义权限类型允许你创建超出内置权限(readwritecreatedeletesubmit)之外的权限标志。可用于特定操作的权限,例如“审批记录”、“下载文件”或“模拟用户”。

工作原理

共分为三个步骤:

  1. 开发者创建权限类型:在开发模式下为 DocType 创建权限类型
  2. 开发者添加权限检查:在操作发生的代码中添加权限检查
  3. 系统管理员分配权限给角色:在 Desk 界面中为角色分配权限

权限类型会作为 fixture 保存,并随你的应用一起发布。

示例:在发票上创建“审批”权限

步骤 1:创建权限类型(开发者)

  1. 启用开发者模式并以管理员身份登录
  2. 创建一条新的 权限类型 记录
  3. 权限类型 设置为 approve,将 DocType 设置为 Invoice
  4. 保存

权限类型现已创建,并将随你的应用一起导出。

步骤 2:在代码中添加权限检查(开发者)

在你的发票控制器或处理审批的函数中,检查该权限:

@frappe.whitelist()
def approve_invoice(invoice_name):
    invoice = frappe.get_doc("Invoice", invoice_name)
    if not frappe.has_permission(invoice, "approve"):
        frappe.throw("Not permitted to approve", frappe.PermissionError)
    invoice.approval_status = "Approved"
    invoice.save()

你也可以在不抛出错误的情况下进行检查:

if frappe.has_permission(invoice, "approve"):
    # show approve button on frontend

步骤 3:分配给角色(系统管理员)

应用安装后,系统管理员将权限分配给角色:

  1. 打开 角色权限管理器
  2. 选择发票 DocType
  3. 对于每个角色,勾选或取消勾选“审批”列,操作方式与其他权限相同
  4. 该权限现在将应用于所有拥有该角色的用户

就这样。拥有“审批”权限的用户现在可以执行该操作。没有该权限的用户将看到错误提示。

如何检查权限

在你的 Python 代码中使用 frappe.has_permission()

# Check document-level permission
if frappe.has_permission(invoice_doc, "approve"):
    # User can approve this invoice

# Check DocType-level permission
if frappe.has_permission("Invoice", "approve"):
    # User can approve any invoice

数据脱敏

注意:此功能仅在夜间版(v16)中可用。此功能被视为实验性功能。

在 Frappe 框架中,可以使用权限级别来控制字段的可见性。

然而,在许多业务场景中,显示某些字段但同时隐藏其敏感数据非常重要。例如:

  • 人力资源用户可能需要查看员工详细信息,但不应看到薪资数额。
  • 支持人员可能会看到以掩码格式显示的客户电话号码,如 811XXXXXXX
  • 财务用户可以查看银行账户字段,而无需暴露完整的账号。

这就是数据掩码的作用所在。

使用数据掩码,您可以根据用户的角色和权限配置特定字段以显示掩码或隐藏的值——而无需限制字段的可见性。
它确保敏感信息得到保护,同时保持用户界面的一致性和信息丰富性。

启用数据掩码

可以直接从 DocType 或通过自定义表单启用数据掩码。

启用数据掩码的步骤

  1. 打开要启用掩码的 DocType(在开发者模式下)或自定义表单。
  2. 选择要掩码的字段。
  3. 勾选“掩码”复选框。
  4. 保存并重新加载表单。

启用后,没有该字段“掩码”权限的用户将看到掩码或隐藏的数据,而具有相应权限的用户将看到实际值。

以下是一个快速演示,展示如何启用数据掩码:

支持的字段类型

数据掩码只能应用于以下字段类型:

SelectRead OnlyPhonePercentPasswordLinkIntFloat
Dynamic LinkDurationDatetimeCurrencyDataDate

面向开发者

Frappe 框架中的数据掩码与现有的权限系统无缝协作。
当用户对某个字段没有所需的“掩码”权限时,框架会自动在用户界面和后端响应中将实际值替换为掩码版本。

内部工作原理

  1. 字段配置
    每个字段都可以在其 DocField 中启用 mask 属性。

    {
        "fieldname": "phone_number",
        "fieldtype": "Data",
        "options": "Phone",
        "mask": 1
    }
    
  2. 权限检查
    系统使用以下方法检查当前用户是否具有该字段的 mask 权限:

    meta.has_permlevel_access_to(fieldname=df.fieldname, df=df, permission_type="mask")
    
  3. 响应中的自动掩码
    一旦权限检查失败,框架会在返回字段值之前自动对其进行掩码处理。
    这适用于:

    • 表单视图数据加载
    • 列表视图查询
    • 使用 ORM 或标准数据获取的报表
    • API 响应(例如 /api/resource/.../api/method/...

注意:数据掩码不会自动应用于使用原始 SQL 的自定义 SQL 查询或查询报表。
在这种情况下,开发者需要在返回响应之前,在查询结果中显式应用掩码逻辑。

自定义文档类型

如果您在多个站点(租户)中使用同一个应用程序,每个站点可能希望在 DocType 之上进行特定的自定义。例如,如果您有一个“客户”DocType,每个用户可能希望添加自定义字段、命名或其他针对他们特定的配置。

为了实现站点特定的自定义,Frappe Framework 提供了多种方法:

  1. 自定义字段:用于跟踪站点特定字段的 DocType。
  2. 属性设置器:用于跟踪 DocType 及其子项中被覆盖的特定属性。
  3. 自定义表单:帮助您轻松自定义 DocType 的视图。
  4. 客户端脚本:额外的客户端事件处理程序。
  5. 服务器脚本:额外的服务器端业务逻辑。
  6. 自定义 DocPerm:额外的权限(通过角色权限管理器处理)

自定义表单

自定义表单是一个视图,可帮助您通过单一视图覆盖 DocType 的属性并添加自定义字段。

当您通过自定义表单更改 DocType 的任何属性时,它不会更改底层的 DocType,而是添加新的自定义对象来覆盖这些属性。这是以无缝方式完成的。

在版本 13 中新增

您还可以通过自定义表单添加/编辑链接和操作。这些更改保存在相同的 DocType(DocType LinkDocType Action)中,但会勾选 custom 属性。

这些额外的(自定义)配置会在通过 frappe.get_meta 获取元数据时自动应用。

操作与链接

在版本 12.1 中新增

操作和链接(也称为连接)是为终端用户提供与文档更多交互的两种方式。下图展示了它们是什么:

操作

一个 DocType 可能有一些 DocType Action,这会导致在 DocType 视图上生成一个按钮。支持的操作有:

  1. 服务器操作:这将触发一个已列入白名单的服务器操作。
  2. 路由:这将重定向到给定的路由。

操作配置

在自定义应用中配置操作

要在您自己的应用中调用操作,您需要一个使用 frappe.whitelist 装饰的 Python 函数:

import frappe

@frappe.whitelist()
def execute_function(*args,**kwargs):
    """
 This function will be executed when the Execute Action Button will be clicked
 """
    print('Hello World')
    # The data is transmitted via keyword argument
    print(kwargs)

此代码应放在您应用中的某个位置,通常放在类似 apps/my_app/my_app/api.py 的文件中

然后,配置相应的操作路径:

连接(关联文档)

DocType 视图的一个标准导航辅助工具是仪表板上的 Connections 部分。这有助于查看者一目了然地识别哪些文档类型与此 DocType 相关联,并可以快速创建新的相关文档。

这些链接还支持添加内部链接(指向子表中的 DocType 的链接)。

配置连接

通过脚本

要为您的应用中的文档类型配置连接,请在 <doctype>_dashboard.py</doctype> 中创建一个 get_data() 函数。以下示例来自 ERPNext 中销售发票文档类型的 sales_invoice_dashboard.py

from frappe import _

def get_data():
    return {
        "fieldname": "sales_invoice",
        "non_standard_fieldnames": {
            "Delivery Note": "against_sales_invoice",
            "Journal Entry": "reference_name",
            "Payment Entry": "reference_name",
            "Payment Request": "reference_name",
            "Sales Invoice": "return_against",
            "Auto Repeat": "reference_document",
            "Purchase Invoice": "inter_company_invoice_reference",
        },
        "internal_links": {
            "Sales Order": ["items", "sales_order"],
            "Timesheet": ["timesheets", "time_sheet"],
        },
        "internal_and_external_links": {
            "Delivery Note": ["items", "delivery_note"],
        },
        "transactions": [
            {
                "label": _("Payment"),
                "items": [
                    "Payment Entry",
                    "Payment Request",
                    "Journal Entry",
                    "Invoice Discounting",
                    "Dunning",
                ],
            },
            {"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]},
            {"label": _("Returns"), "items": ["Sales Invoice"]},
            {"label": _("Subscription"), "items": ["Auto Repeat"]},
            {"label": _("Internal Transfers"), "items": ["Purchase Invoice"]},
        ],
    }
  • transactions 定义所有连接以及它们所属的相应分组。
  • internal_links 定义文档类型具有内部链接的连接。例如,销售发票通过其项目子表与销售订单文档类型具有内部链接。
  • internal_and_external_links 定义文档类型同时具有内部和外部链接的连接。例如,销售发票通过其项目子表与交货单文档类型具有内部链接,并且销售发票也通过其项目子表与交货单文档类型具有外部链接。
  • fieldname 定义在查找文档类型的外部链接时要搜索的默认字段名。
  • non_standard_fieldnames 定义在查找文档类型的外部链接时要搜索的字段名及其相应的文档类型。

这将产生以下连接:

DocType 操作和链接可通过自定义表单进行扩展