公共工具API

frappe.get_route

frappe.get_route()

将当前路由以数组形式返回。

frappe.get_route()
// ["List", "Task", "List"]

frappe.set_route

frappe.set_route(route)

将当前路由更改为 route

// route in parts
frappe.set_route('List', 'Task', 'List')

// route as array
frappe.set_route(['List', 'Task', 'Gantt'])

// route as string
frappe.set_route('List/Event/Calendar')

// route with options
frappe.set_route(['List', 'Task', 'Task'], { status: 'Open' })

frappe.format

frappe.format(value, df, options, doc)

将原始值格式化为用户可读的展示格式。

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

frappe.format('2399', { fieldtype: 'Currency', options: 'currency' }, { inline: true })
// "2,399.00"

frappe.provide

frappe.provide(namespace)

如果 window 对象上不存在该命名空间,则创建并挂载到 window 对象上。

frappe.provide('frappe.ui.form');

// has the same effect as
window.frappe = {}
window.frappe.ui = {}
window.frappe.ui.form = {}

frappe.require

frappe.require(asset_path, callback)

异步加载 JS 或 CSS 资源。用于加载不常用的库。

// load a single asset
frappe.require('/assets/frappe/chat.js', () => {
 // chat.js is loaded
})

// load multiple assets
frappe.require(['/assets/frappe/chat.js', '/assets/frappe/chat.css'], () => {
 // chat.js and chat.css are loaded
})

树形视图是为所有启用了“是树形”属性的 DocType 生成的。


树形视图

标准树形 JS

要自定义树形视图,你必须在 doctype 目录中有一个 {doctype}_tree.js 文件。
以下是所有可以自定义的选项。

例如,如果你想配置 Account DocType,你需要创建一个包含以下内容的 account_tree.js 文件。

frappe.treeview_settings["Account"] = {
    breadcrumb: "Accounting",
    title: "Chart of Accounts",

    filters: [
        {
            fieldname: "company",
            fieldtype: "Select",
            label: "Company",
            options: "Company 1\nCompany 2",
            on_change: handle_company_change(),
        },
    ],

    get_tree_nodes: "path.to.whitelisted_method.get_children",
    add_tree_node: "path.to.whitelisted_method.handle_add_account",

    fields: [
        {
            fieldtype: "Data",
            fieldname: "account_name",
            label: "New Account Name",
            reqd: true,
        },
        {
            fieldtype: "Link",
            fieldname: "account_currency",
            label: "Currency",
            options: "Currency",
        },
        {
            fieldtype: "Check",
            fieldname: "is_group",
            label: "Is Group",
        },
    ],

    ignore_fields: ["parent_account"],

    menu_items: [
        {
            label: "New Company",
            action: function () {
                frappe.new_doc("Company", true);
            },
            condition: "frappe.boot.user.can_create.indexOf('Company') !== -1",
        },
    ],

    onload: function (treeview) {},

    post_render: function (treeview) {},

    onrender: function (node) {},

    on_get_node: function (nodes) {},

    extend_toolbar: true,

    toolbar: [
        {
            label: "Add Child",
            condition: function (node) {
                return node && node.is_group;
            },
            click: function (node) {
                frappe.treeview_settings["Account"].add_node(node);
            },
            btnClass: "hidden-xs",
        },
    ],
};

页面接口

Desk 中的每个屏幕都渲染在一个 frappe.ui.Page 对象内。

frappe.ui.make_app_page

创建一个新的页面并将其附加到父级。

let page = frappe.ui.make_app_page({
    title: 'My Page',
    parent: wrapper // HTML DOM Element or jQuery object
    single_column: true // create a page without sidebar
})

新建页面

页面方法

本节列出了页面实例对象上可用的常用方法。

page.set_title

设置页面标题以及文档标题。文档标题显示在浏览器标签页中。

page.set_title('My Page')

页面标题

page.set_title_sub

设置页面的副标题。它显示在页面标题的右侧。

page.set_title_sub('Subtitle')

页面副标题

page.set_indicator

设置指示器的标签和颜色。

page.set_indicator('Pending', 'orange')

页面指示器

page.clear_indicator

清除指示器的标签和颜色。

page.clear_indicator()

page.set_primary_action

设置主要操作按钮的标签和处理函数。第三个参数是图标类,将在移动视图中显示。

let $btn = page.set_primary_action('New', () => create_new(), 'octicon octicon-plus')

页面主要操作

page.clear_primary_action

清除主要操作按钮和处理函数。

page.clear_primary_action()

page.set_secondary_action

设置次要操作按钮的标签和处理函数。第三个参数是图标类,将在移动视图中显示。

let $btn = page.set_secondary_action('Refresh', () => refresh(), 'octicon octicon-sync')

页面次要操作

page.clear_secondary_action

清除次要操作按钮和处理函数。

page.clear_secondary_action()

page.add_menu_item

在菜单下拉框中添加菜单项。

// add a normal menu item
page.add_menu_item('Send Email', () => open_email_dialog())

// add a standard menu item
page.add_menu_item('Send Email', () => open_email_dialog(), true)

页面菜单下拉框

page.clear_menu

移除包含菜单项的下拉菜单。

page.clear_menu()

page.add_action_item

在操作下拉框中添加菜单项。

// add a normal menu item
page.add_action_item('Delete', () => delete_items())

页面操作下拉框

page.clear_actions_menu

移除包含菜单项的操作下拉菜单。

page.clear_actions_menu()

page.add_inner_button

在内部工具栏中添加按钮。

// add a normal inner button
page.add_inner_button('Update Posts', () => update_posts())

页面内部按钮

// add a dropdown button in a group
page.add_inner_button('New Post', () => new_post(), 'Make')

页面内部按钮组

page.change_custombuttontype

通过标签(和组)更改特定自定义按钮的类型。

// change type of ungrouped button
page.change_inner_button_type('Update Posts', null, 'primary');

// change type of a button in a group
page.change_inner_button_type('Delete Posts', 'Actions', 'danger');

page.remove_inner_button

移除内部工具栏中的按钮。

// remove inner button
page.remove_inner_button('Update Posts')

// remove dropdown button in a group
page.remove_inner_button('New Posts', 'Make')

page.clear_inner_toolbar

移除内部工具栏。

page.clear_inner_toolbar()

page.add_field

在页面表单工具栏中添加一个表单控件。

let field = page.add_field({
    label: 'Status',
    fieldtype: 'Select',
    fieldname: 'status',
    options: [
        'Open',
        'Closed',
        'Cancelled'
    ],
    change() {
        console.log(field.get_value());
    }
});

页面表单工具栏

page.get_form_values

以对象形式获取页面表单工具栏中的所有表单值。

let values = page.get_form_values()
// { status: 'Open', priority: 'Low' }

page.clear_fields

清除页面表单工具栏中的所有字段。

page.clear_fields()

列表

列表视图是为除子表和单文档类型之外的所有文档类型生成的。

列表视图功能丰富,其中一些功能包括:

  • 筛选
  • 排序
  • 分页
  • 按标签筛选
  • 切换视图为报表、日历、甘特图、看板等。

列表视图

标准列表 JS

要自定义列表视图,您必须在文档类型目录中有一个 {doctype}_list.js 文件。以下是所有可以自定义的选项。

例如,如果您想自定义 Note 文档类型,您需要创建一个包含以下内容的 note_list.js 文件。

frappe.listview_settings['Note'] = {
    // add fields to fetch
    add_fields: ['title', 'public'],
    // set default filters
    filters: [
        ['public', '=', 1]
    ],
    hide_name_column: true, // hide the last column which shows the `name`
    hide_name_filter: true, // hide the default filter field for the name column
    onload(listview) {
        // triggers once before the list is loaded
    },
    before_render() {
        // triggers before every render of list records
    },

    // set this to true to apply indicator function on draft documents too
    has_indicator_for_draft: false,

    get_indicator(doc) {
        // customize indicator color
        if (doc.public) {
            return [__("Public"), "green", "public,=,Yes"];
        } else {
            return [__("Private"), "darkgrey", "public,=,No"];
        }
    },
    primary_action() {
        // triggers when the primary action is clicked
    },
    get_form_link(doc) {
        // override the form route for this doc
    },
    // add a custom button for each row
    button: {
        show(doc) {
            return doc.reference_name;
        },
        get_label() {
            return 'View';
        },
        get_description(doc) {
            return __('View {0}', [`${doc.reference_type} ${doc.reference_name}`])
        },
        action(doc) {
            frappe.set_route('Form', doc.reference_type, doc.reference_name);
        }
    },
    // format how a field value is shown
    formatters: {
        title(val) {
            return val.bold();
        },
        public(val) {
            return val ? 'Yes' : 'No';
        }
    }
}

自定义列表 JS

您还可以通过创建系统中的客户端脚本来自定义列表视图。如果逻辑特定于您的站点,您应该编写客户端脚本。如果您想在多个站点之间共享列表视图自定义设置,则必须通过应用程序来包含它们。

要创建新的客户端脚本,请前往

首页 > 自定义 > 客户端脚本 > 新建

用于列表的新客户端脚本

上述自定义将产生如下所示的列表视图:

自定义列表视图

列表 JS 中的多个按钮

此功能在开发版中可用。

您现在可以通过列表视图客户端脚本,在列表视图行中的下拉菜单内添加多个按钮。此功能通过直接从列表视图方便地访问各种操作,增强了用户体验。

frappe.listview_settings["ToDo"] = {
    hide_name_column: true,
    add_fields: ["reference_type", "reference_name"],

    button: {
      show: function(doc) {
        return doc.reference_name;
      },
      get_label: function() {
        return __("Open", null, "Access");
      },
      get_description: function(doc) {
        return __("Open {0}", [
          `${__(doc.reference_type)}: ${doc.reference_name}`
        ]);
      },
      action: function(doc) {
        frappe.set_route("Form", doc.reference_type, doc
          .reference_name);
      },
    },
    dropdown_button: {
      get_label: __("Dropdown"),
      buttons: [{
          get_label: __("Button 1"),
          show: function(doc) {
            return true;
          },
          get_description: function(doc) {
            return "Open Button 1 " + doc.reference_name;
          },
          action: function(doc) {
            frappe.msgprint("Dropdown Button 1 Clicked " +
              doc.reference_name);
          }
        },
        {
          get_label: __("Button 2"),
          show: function(doc) {
            return doc.status != "Closed";
          },
          get_description: function(doc) {
            return "Open Button 2 " + doc.reference_name;
          },
          action: function(doc) {
            frappe.msgprint("Dropdown Button 2 Clicked " +
              doc.reference_name);
          }
        },
        {
          get_label: __("Button 3"),
          show: function(doc) {
            return doc.status != "Cancelled";
          },
          get_description: function(doc) {
            return "Open Button 3 " + doc.reference_name;
          },
          action: function(doc) {
            frappe.msgprint("Dropdown Button 3 Clicked " +
              doc.reference_name);
          }
        },
      ]
    }
  };

控件

frappe.ui.form.make_control

frappe.ui.form.make_control({ parent, df })

根据 df 属性创建一个 frappe 控件,并将其追加到 parent
容器中。

frappe.ui.form.make_control({
 parent: $wrapper.find('.my-control'),
 df: {
 label: 'Due Date',
 fieldname: 'due_date',
 fieldtype: 'Date'
 },
 render_input: true
})

以下是大多数 frappe 控件类型的 df 属性。

// Attach
{
 label: 'Attachment',
 fieldname: 'attachment',
 fieldtype: 'Attach'
}

// Attach Image
{
 label: 'User Image',
 fieldname: 'user_image',
 fieldtype: 'Attach Image'
}

// Autocomplete
{
 label: 'Select User',
 label: 'user',
 fieldtype: 'Autocomplete',
 options: [
 '[email protected]',
 '[email protected]'
 ]
}

// Barcode
{
 label: 'Item Barcode',
 fieldname: 'item_barcode',
 fieldtype: 'Barcode'
}

// Check
{
 label: 'Enable feature',
 fieldname: 'enable_feature',
 fieldtype: 'Check'
}

// Code
{
 label: 'JS Script',
 fieldname: 'script',
 fieldtype: 'Code',
 // for syntax highlighting
 options: 'Javascript' // JS, HTML, CSS, Markdown, SCSS, JSON,
 // wrap code
 wrap: true,
 // changing `max_lines` will set the max-height of the editor
 max_lines: 10,
 // changing `min_lines` will set the min-height of the editor
 min_lines: 5
}

// Color
{
 label: 'Your favorite color',
 fieldname: 'user_color',
 fieldtype: 'Color'
}

// Currency
{
 label: 'Amount',
 fieldname: 'amount',
 fieldtype: 'Currency',
 options: 'INR' // or name of field which holds currency
}

// Data
{
 label: 'First Name',
 fieldname: 'first_name',
 fieldtype: 'Data',
 options: 'Email' // 'Name', 'Phone', 'URL', 'Barcode'
}

// Date Range
{
 label: 'Select Date Range',
 fieldname: 'date_range',
 fieldtype: 'Date Range'
}

// Date
{
 label: 'Birth Date',
 fieldname: 'birth_date',
 fieldtype: 'Date'
}

// Datetime
{
 label: 'Submission Date and Time',
 fieldname: 'submission',
 fieldtype: 'Datetime'
}

// Dynamic Link
{
 label: 'Party',
 fieldname: 'party',
 fieldtype: 'Dynamic Link',
 options: 'party_type' // fieldname which holds the Link type
}

// Float
{
 label: 'Threshold',
 fieldname: 'threshold',
 fieldtype: 'Float'
}

// Geolocation
{
 label: 'Meeting Place',
 fieldname: 'meeting_place',
 fieldtype: 'Geolocation'
}

// HTML Editor
{
 label: 'Custom HTML',
 fieldname: 'custom_html',
 fieldtype: 'HTML Editor'
}

// Int
{
 label: 'No of days',
 fieldname: 'no_of_days',
 fieldtype: 'Int'
}

// Link
{
 label: 'Select User',
 fieldname: 'user',
 fieldtype: 'Link',
 options: 'User' // name of doctype
}

// Markdown Editor
{
 label: 'Blog Content',
 fieldname: 'content',
 fieldtype: 'Markdown Editor'
}

// MultiCheck
{
 label: 'Blog Content',
 fieldname: 'content',
 fieldtype: 'MultiCheck',
 options: [
 'Option 1',
 'Option 2',
 'Option 3',
 'Option 4',
 ],
 columns: 2 // break into 2 columns
}

// MultiSelect
{
 label: 'Select Users',
 fieldname: 'users',
 fieldtype: 'MultiSelect',
 options: [
 '[email protected]',
 '[email protected]',
 '[email protected]'
 ]
}

// Password
{
 label: 'New Password',
 fieldname: 'password',
 fieldtype: 'Password'
}

// Rating
{
 label: 'Rate your experience',
 fieldname: 'rating',
 fieldtype: 'Rating'
}

// Select
{
 label: 'Status',
 fieldname: 'status',
 fieldtype: 'Select',
 options: [
 'Open',
 'Closed',
 'Cancelled'
 ]
}

// Signature
{
 label: 'Status',
 fieldname: 'status',
 fieldtype: 'Signature'
}

// Text Editor
{
 label: 'Description',
 fieldname: 'description',
 fieldtype: 'Text Editor'
}

// Time
{
 label: 'In Time',
 fieldname: 'in_time',
 fieldtype: 'Time'
}

// Button
{
 label: 'Fetch',
 fieldname: 'fetch',
 fieldtype: 'Button',
 btn_size: 'xs' // xs, sm, lg
}

//Icon
{
 label: 'Page Icon',
 fieldname: 'page_icon',
 fieldtype: 'Icon'
}

添加自定义格式化器

您可以通过将自定义格式化器添加到 frappe.meta.docfield_map 中的 docfield 对象,为文本类型对象(如 Data、Select、Text 等)添加自定义格式化器。

示例:

frappe.meta.docfield_map['DocField'].fieldtype.formatter = (value) => {
 if (value==='Section Break') return '🔵 Section Break';
 else return value;
}

表单脚本

表单脚本(Form Scripts)允许您为表单添加客户端逻辑。您可以编写表单脚本来自动获取值、添加验证或为表单添加上下文操作。

标准表单脚本

当您创建新的 DocType 时,系统会生成一个 {doctype}.js,您可以在其中编写表单脚本。

语法:

frappe.ui.form.on(doctype, {
    event1() {
        // handle event 1
    },
    event2() {
        // handle event 2
    }
})

例如,位于 frappe/desk/doctype/todo/todo.jstodo.js 可能如下所示:

// Script for ToDo Form
frappe.ui.form.on('ToDo', {
    // on refresh event
    refresh(frm) {
        // if reference_type and reference_name are set,
        // add a custom button to go to the reference form
        if (frm.doc.reference_type && frm.doc.reference_name) {
            frm.add_custom_button(__(frm.doc.reference_name), () => {
                frappe.set_route("Form", frm.doc.reference_type, frm.doc.reference_name);
            });
        }
    }
})

子表脚本

子表脚本应与其父表脚本写在同一个文件中。

frappe.ui.form.on('Quotation', {
    // ...
})

frappe.ui.form.on('Quotation Item', {
    // cdt is Child DocType name i.e Quotation Item
    // cdn is the row name for e.g bbfcb8da6a
    item_code(frm, cdt, cdn) {
        let row = frappe.get_doc(cdt, cdn);
    }
})

自定义表单脚本

您还可以通过在系统中创建客户端脚本(Client Script)来编写表单脚本。如果逻辑是针对您特定站点的,则应编写客户端脚本。如果您希望跨站点共享表单脚本,则必须通过应用(Apps)来包含它们。

要创建新的客户端脚本,请前往

首页 > 自定义 > 客户端脚本 > 新建

新建表单客户端脚本

表单事件

表单脚本依赖事件来触发。以下是表单触发的所有表单事件的列表。

这些事件的处理函数会将 frm 作为第一个参数接收。

frappe.ui.form.on('ToDo', {
    // frm passed as the first parameter
    setup(frm) {
        // write setup code
    }
})
事件名称 描述
setup 当表单首次创建时触发一次
before_load 在表单即将加载之前触发
onload 当表单已加载并即将渲染时触发
refresh 当表单已加载并渲染完成时触发。
onload_post_render 在表单加载并渲染完成后触发
validate 在 before_save 之前触发
before_save 在调用保存之前触发
after_save 在表单保存后触发
before_submit 在调用提交之前触发
on_submit 在表单提交后触发
before_cancel 在调用取消之前触发
after_cancel 在表单取消后触发
before_discard 在调用放弃之前触发
after_discard 在表单被放弃后触发
timeline_refresh 在表单时间线渲染后触发
{fieldname}_on_form_rendered 当表格字段中的某行作为表单打开时触发
{fieldname} 当字段名的值发生变化时触发
get_email_recipient_filters 由电子邮件对话框调用,用于获取电子邮件收件人的默认筛选条件。应接受两个参数 frm(当前表单)和 field(”recipients”、”cc” 或 “bcc”),并返回一个数组或字典形式的筛选条件(与联系人(Contact) DocType 相关)。
get_email_recipients 由电子邮件对话框调用,用于获取默认收件人。应接受两个参数 frm(当前表单)和 field(”recipients”、”cc” 或 “bcc”),并返回该字段的电子邮件地址列表。

子表事件

这些事件在子表的上下文中触发。因此,除了 frm 之外,它们的处理函数还会接收 cdt(子 DocType)和 cdn(子 Docname)参数。

假设我们的“待办事项(ToDo)”DocType 有一个名为“链接(links)”的字段,其中包含一个子表。该子表在名为“动态链接(Dynamic Link)”的 DocType 中定义。我们希望每当向该表添加一行时都运行我们的代码。

// this code is located inside `todo.js`

frappe.ui.form.on('Dynamic Link', { // The child table is defined in a DoctType called "Dynamic Link"
    links_add(frm, cdt, cdn) { // "links" is the name of the table field in ToDo, "_add" is the event
        // frm: current ToDo form
        // cdt: child DocType 'Dynamic Link'
        // cdn: child docname (something like 'a6dfk76')
        // cdt and cdn are useful for identifying which row triggered this event

        frappe.msgprint('A row has been added to the links table 🎉 ');
    }
});
事件名称 描述
before_{fieldname}_remove 当一行即将从表格字段中移除时触发
{fieldname}_add 当向表格字段添加一行时触发
{fieldname}_remove 当从表格字段中移除一行时触发
{fieldname}_move 当一行在表格字段中被重新排序到其他位置时触发
form_render 当表格字段中的某行作为表单打开时触发

注意:上表列出的前三个事件,即 before_{fieldname}_remove{fieldname}_add{fieldname}_remove,也会针对字段类型为表格多选(Table MultiSelect)的字段触发。(自版本 16 起)

表单 API

以下是 frm 对象上可用的一些常用方法列表。

frm.set_value

设置字段的值。这将触发表单中的字段更改事件。

// set a single value
frm.set_value('description', 'New description')

// set multiple values at once
frm.set_value({
    status: 'Open',
    description: 'New description'
})

// returns a promise
frm.set_value('description', 'New description')
    .then(() => {
        // do something after value is set
    })

frm.refresh

使用服务器上的最新值刷新表单。将触发 before_loadonloadrefreshtimeline_refreshonload_post_render

frm.refresh();

frm.save

触发表单保存。将触发 validatebefore_saveafter_savetimeline_refreshrefresh

它可以用来触发其他保存操作,如提交、取消和更新。在这种情况下,相关事件将被触发。

// save form
frm.save();

// submit form
frm.save('Submit');

// cancel form
frm.save('Cancel');

// update form (after submit)
frm.save('Update');

// all methods returns a promise

frm.enable_save / frm.disable_save

用于启用/禁用表单中“保存”按钮的方法。

if (frappe.user_roles.includes('Custom Role')) {
    frm.enable_save();
} else {
    frm.disable_save();
}

frm.email_doc

打开此表单的电子邮件对话框。

// open email dialog
frm.email_doc();

// open email dialog with some message
frm.email_doc(`Hello ${frm.doc.customer_name}`);

frm.reload_doc

使用服务器上的最新值重新加载文档,并调用 frm.refresh()

frm.reload_doc();

frm.refresh_field

刷新字段及其依赖项。

frm.refresh_field('description');

frm.is_dirty

检查表单值是否已更改且尚未保存。

if (frm.is_dirty()) {
    frappe.show_alert('Please save form before attaching a file')
}

frm.dirty

将表单设置为“脏”状态。这用于在文档值更改时将表单标记为脏状态。这会触发表单视图中的“未保存”指示器。

frm.doc.browser_data = navigator.appVersion;
frm.dirty();
frm.save();

在不将表单设置为脏状态的情况下调用保存,将触发“文档无更改”的提示消息。

frm.is_new

检查表单是否为新建且尚未保存。

// add custom button only if form is not new
if (!frm.is_new()) {
    frm.add_custom_button('Click me', () => console.log('Clicked custom button'))
}

frm.set_intro

在表单顶部设置介绍文本。该函数接受两个参数:message(字符串,必填)和 color(字符串,可选)。

颜色可以是“蓝色”、“红色”、“橙色”、“绿色”或“黄色”。默认是蓝色。

if (!frm.doc.description) {
    frm.set_intro('Please set the value of description', 'blue');
}

介绍文本示例

frm.add_custom_button

在页面的内部工具栏中添加自定义按钮。是 page.add_inner_button 的别名。

// Custom buttons
frm.add_custom_button('Open Reference form', () => {
    frappe.set_route('Form', frm.doc.reference_type, frm.doc.reference_name);
})

// Custom buttons in groups
frm.add_custom_button('Closed', () => {
    frm.doc.status = 'Closed'
}, 'Set Status');

frm.change_custom_button_type

通过标签(和组)更改特定自定义按钮的类型。

// change type of ungrouped button
frm.change_custom_button_type('Open Reference form', null, 'primary');

// change type of a button in a group
frm.change_custom_button_type('Closed', 'Set Status', 'danger');

frm.remove_custom_button

通过标签(和组)移除特定的自定义按钮。

// remove custom button
frm.remove_custom_button('Open Reference form');

// remove custom button in a group
frm.remove_custom_button('Closed', 'Set Status');

frm.clear_custom_buttons

从内部工具栏中移除所有自定义按钮。

frm.clear_custom_buttons();

frm.set_df_property

更改字段的文档字段属性并刷新该字段。

// change the fieldtype of description field to Text
frm.set_df_property('description', 'fieldtype', 'Text');

// set the options of the status field to only be [Open, Closed]
frm.set_df_property('status', 'options', ['Open', 'Closed'])

// set a field as mandatory
frm.set_df_property('title', 'reqd', 1)

// set a field as read only
frm.set_df_property('status', 'read_only', 1)

frm.toggle_enable

根据条件将字段或字段列表切换为 read_only 状态。

// set status and priority as read_only
// if user does not have System Manager role
let is_allowed = frappe.user_roles.includes('System Manager');
frm.toggle_enable(['status', 'priority'], is_allowed);

frm.toggle_reqd

根据条件将字段或字段列表切换为必填(reqd)状态。

// set priority as mandatory
// if status is Open
frm.toggle_reqd('priority', frm.doc.status === 'Open');

frm.toggle_display

根据条件显示/隐藏字段或字段列表。

// show priority and due_date field
// if status is Open
frm.toggle_display(['priority', 'due_date'], frm.doc.status === 'Open');

frm.set_query

对链接字段应用过滤器,以显示有限的记录供选择。您必须在表单生命周期的早期调用 frm.set_query,通常是在 setuponload 中。

// show only customers whose territory is set to India
frm.set_query('customer', () => {
    return {
        filters: {
            territory: 'India'
        }
    }
})

// show customers whose territory is any of India, Nepal, Japan
frm.set_query('customer', () => {
    return {
        filters: {
            territory: ['in', ['India', 'Nepal', 'Japan']]
        }
    }
})

// set filters for Link field item_code in
// items field which is a Child Table
frm.set_query('item_code', 'items', () => {
    return {
        filters: {
            item_group: 'Products'
        }
    }
})

您还可以覆盖过滤方法,并在服务器端提供自己的自定义方法。只需将查询设置为您的 Python 方法的模块路径即可。

// change the filter method by passing a custom method
frm.set_query('fieldname', () => {
    return {
        query: 'dotted.path.to.custom.custom_query',
        filters: {
            field1: 'value1'
        }
    }
})
# python method signature
def custom_query(doctype, txt, searchfield, start, page_len, filters):
    # your logic
    return filtered_list

frm.add_child

向表格字段添加一行带值的记录。

let row = frm.add_child('items', {
    item_code: 'Tennis Racket',
    qty: 2
});

frm.refresh_field('items');

frm.call

使用参数调用服务器端的控制器方法。

注意: 在使用 frm.call() 访问任何服务器端方法时,您需要使用 @frappe.whitelist 装饰器将该方法列入白名单。

对于以下控制器代码:

class ToDo(Document):
    @frappe.whitelist()
    def get_linked_doc(self, throw_if_missing=False):
        if not frappe.db.exists(self.reference_type, self.reference_name):
            if throw_if_missing:
                frappe.throw('Linked document not found')

        return frappe.get_doc(self.reference_type, self.reference_name)

您可以使用 frm.call 从客户端调用它。

frm.call('get_linked_doc', { throw_if_missing: true })
    .then(r => {
        if (r.message) {
            let linked_doc = r.message;
            // do something with linked_doc
        }
    })

frm.trigger

显式触发任何表单事件。

frappe.ui.form.on('ToDo', {
    refresh(frm) {
        frm.trigger('set_mandatory_fields');
    },

    set_mandatory_fields(frm) {
        frm.toggle_reqd('priority', frm.doc.status === 'Open');
    }
})

frm.get_selected

在子表中获取选中的行,返回一个对象,其中键是表格字段名,值是行名称。

let selected = frm.get_selected()
console.log(selected)
// {
// items: ["bbfcb8da6a", "b1f1a43233"]
// taxes: ["036ab9452a"]
// }

frm.ignore_doctypes_on_cancel_all

为避免在全部取消期间取消链接文档,您需要将 frm.ignored_doctypes_on_cancel_all 属性设置为链接文档的 DocTypes 数组。

frappe.ui.form.on("DocType 1", {
    onload: function(frm) {
        // Ignore cancellation for all linked documents of respective DocTypes.
        frm.ignore_doctypes_on_cancel_all = ["DocType 2", "DocType 3"];
    }
}

在上述示例中,系统将在取消期间避免取消所有与“DocType 1”文档链接的“DocType 2”和“DocType 3”文档。

开发者API

Python

Frappe 旨在为其用户实现最低的认知负担。因此,你可以在
frappe 命名空间本身中找到最常用的方法和工具。在 Python 文件中,
这(大多数情况下)是你唯一需要的导入。

  1. 文档
  2. 数据库
  3. Jinja 模板
  4. 常用工具
  5. 路由器
  6. 响应
  7. 语言解析
  8. 搜索
  9. 钩子
  10. REST API
  11. 全文搜索
  12. 对话框 API
  13. 查询构建器

Javascript

Frappe 将自身挂载到 window 对象下的 frappe 命名空间中。你
可以在 frappe 对象下找到大部分客户端 API。所有这些方法
仅在 Desk 环境中可用。探索这些 API 的一个好方法是从
浏览器控制台入手。

  1. 表单
  2. 控件
  3. 页面
  4. 树形视图
  5. 服务器调用 (AJAX)
  6. 常用工具
  7. 对话框 API
  8. 图表 API
  9. 扫描器 API

其他

  1. REST API
  2. Jinja API

SQLite 搜索

SQLite 搜索是 Frappe 应用程序的一个全文搜索框架,利用 SQLite 的 FTS5(全文搜索)引擎提供高级搜索功能。它提供了拼写纠正、基于时间的时效性评分、自定义排名、权限感知过滤和可扩展的评分管道等功能。

目录

  • 快速开始

  • 工作原理

  • 配置

  • 功能与自定义

  • API 参考

快速开始

1. 创建搜索类

通过继承 SQLiteSearch 来创建搜索实现:


# my_app/search.py

from frappe.search.sqlite_search import SQLiteSearch

class MyAppSearch(SQLiteSearch):

    # Database file name

    INDEX_NAME = "my_app_search.db"

    # Define the search schema

    INDEX_SCHEMA = {

        "metadata_fields": ["project", "owner", "status"],

        "tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_'",

    }

    # Define which doctypes to index and their field mappings

    INDEXABLE_DOCTYPES = {

        "Task": {

            "fields": ["name", {"title": "subject"}, {"content": "description"}, "modified", "project", "owner", "status"],

        },

        "Issue": {

            "fields": ["name", "title", "description", {"modified": "last_updated"}, "project", "owner"],

            "filters": {"status": ("!=", "Closed")},  # Only index non-closed issues

        },

    }

    def get_search_filters(self):

        """Return permission filters for current user"""

        # Get projects accessible to current user

        accessible_projects = frappe.get_all(

            "Project",

            filters={"owner": frappe.session.user},

            pluck="name"

        )

        if not accessible_projects:

            return {"project": []}  # No access

        return {"project": accessible_projects}

2. 注册搜索类

将你的搜索类添加到 hooks.py 文件中:


# my_app/hooks.py

sqlite_search = ['my_app.search.MyAppSearch']

3. 创建 API 端点

创建一个白名单方法来暴露搜索功能:


# my_app/api.py

import frappe

from my_app.search import MyAppSearch

@frappe.whitelist()

def search(query, filters=None):

    search = MyAppSearch()

    result = search.search(query, filters=filters)

    return result

4. 构建索引

通过编程方式或控制台构建搜索索引:


from my_app.search import MyAppSearch

search = MyAppSearch()

search.build_index()

工作原理

1. 索引过程

完整索引构建

当你调用 build_index() 时,框架会执行一次完整的索引重建:

  1. 数据库准备:创建一个临时的 SQLite 数据库,并根据你的模式配置 FTS5 表

  2. 文档收集:使用配置的字段映射和过滤器查询所有指定的 DocType

  3. 文档处理:对于每个文档:

    • 根据 INDEXABLE_DOCTYPES 配置提取和映射字段

    • 使用 BeautifulSoup 清理 HTML 内容以提取纯文本

    • 如果覆盖了 prepare_document(),则应用自定义的文档准备逻辑

    • 验证必填字段(标题、内容)是否存在

  4. 批量插入:将处理后的文档分批插入 FTS5 索引以提高性能

  5. 词汇表构建:从所有索引文本中构建拼写纠正字典

  6. 原子替换:以原子方式用新数据库替换现有索引数据库

单个文档索引

使用 index_doc()remove_doc() 进行实时更新:

  1. 单个文档处理:使用相同的字段映射逻辑检索并处理一个文档

  2. 增量更新:通过插入、更新或删除特定文档来更新现有的 FTS5 索引

  3. 词汇表更新:使用文档中的新术语更新拼写字典

2. 搜索过程

当用户使用 search() 执行搜索时,框架会执行以下步骤:

  1. 权限过滤:调用 get_search_filters() 来确定当前用户可以访问哪些文档

  2. 查询预处理

    • 验证搜索查询不为空

    • 将用户提供的过滤器与权限过滤器合并

  3. 拼写纠正

    • 对照词汇字典分析查询词条

    • 使用三元组相似度来为拼写错误的单词提供纠正建议

    • 使用纠正后的词条扩展原始查询

  4. FTS5 查询执行

    • 构建一个兼容 FTS5 的查询字符串

    • 对 SQLite 数据库执行全文搜索

    • 应用元数据过滤器(状态、所有者、项目等)

    • 检索带有 BM25 分数的原始结果

  5. 结果处理

    • 自定义评分:应用评分管道来计算最终的相关性分数

      • 基础 BM25 分数处理

      • 标题匹配加权(精确匹配和部分匹配)

      • 基于文档年龄的时效性加权

      • 自定义评分函数(特定 DocType、基于优先级等)

    • 排名:按最终分数对结果进行排序并分配排名位置

    • 内容格式化:生成内容摘要并高亮匹配词条

配置

INDEX_SCHEMA

定义搜索索引的结构:


INDEX_SCHEMA = {

    # Text fields that will be searchable (defaults to ["title", "content"])

    "text_fields": ["title", "content"],

    # Metadata fields stored alongside text content for filtering

    "metadata_fields": ["project", "owner", "status", "priority"],

    # FTS5 tokenizer configuration

    "tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_@.'"

}

INDEXABLE_DOCTYPES

指定要索引的 DocType 以及如何映射其字段:


INDEXABLE_DOCTYPES = {

    "Task": {

        # Field mapping

        "fields": [

            "name",

            {"title": "subject"},        # Maps subject field to title

            {"content": "description"},  # Maps description field to content

            {"modified": "creation"},    # Use creation instead of modified for recency boost

            "project",

            "owner"

        ],

        # Optional filters to limit which records are indexed

        "filters": {

            "status": ("!=", "Cancelled"),

            "docstatus": ("!=", 2)

        }

    }

}

字段映射规则

  • 字符串字段:直接映射 "field_name"

  • 别名字段:字典映射 {"schema_field": "doctype_field"}

  • 必填字段titlecontent 字段必须存在或显式映射(例如 {"title": "subject"}

  • 自动添加的字段doctypename 会自动包含

  • 修改字段:如果在任何文档类型配置中使用,则会自动添加。用于近期加权 – 如果您想使用不同的时间戳字段(如 creationlast_updated),请使用 {"modified": "creation"} 将其映射到 modified

功能与自定义

权限过滤

实现 get_search_filters() 来控制访问权限:


def get_search_filters(self):

    """Return filters based on user permissions"""

    user = frappe.session.user

    if user == "Administrator":

        return {}  # No restrictions

    # Example: User can only see their own and public documents

    return {

        "owner": user,

        "status": ["Active", "Published"]

    }

自定义评分

创建自定义评分函数以影响搜索相关性:


class MyAppSearch(SQLiteSearch):

    ...

    @SQLiteSearch.scoring_function

    def *get*priority_boost(self, row, query, query_words):

        """Boost high-priority items"""

        priority = row.get("priority", "Medium")

        if priority == "High":

            return 1.5

        if priority == "Medium":

            return 1.1

        return 1.0

近期加权

框架使用 modified 字段自动提供基于时间的近期加权:


# The modified field is used for calculating document age

# Recent documents get higher scores:

# - Last 24 hours: 1.8x boost

# - Last 7 days: 1.5x boost

# - Last 30 days: 1.2x boost

# - Last 90 days: 1.1x boost

# - Older documents: gradually decreasing boost

# If your doctype uses a different timestamp field, map it to modified:

INDEXABLE_DOCTYPES = {

    "GP Discussion": {

        "fields": ["name", "title", "content", {"modified": "last_post_at"}, "project"],

    },

    "Article": {

        "fields": ["name", "title", "content", {"modified": "published_date"}, "category"],

    }

}

文档准备

覆盖 prepare_document() 以进行自定义文档处理:


def prepare_document(self, doc):

    """Custom document preparation"""

    document = super().prepare_document(doc)

    if not document:

        return None

    # Add computed fields

    if doc.doctype == "Task":

        # Combine multiple fields into content

        content_parts = [

            doc.description or "",

            doc.notes or "",

            "\n".join([comment.content for comment in doc.get("comments", [])])

        ]

        document["content"] = "\n".join(filter(None, content_parts))

        # set fields that might be stored in another table

        document["category"] = get_category_for_task(doc)

    return document

拼写纠正

框架内置了使用三元组相似度的拼写纠正功能:


# Spelling correction happens automatically

search_result = search.search("projetc managment")  # Will find "project management"

# Access correction information

print(search_result["summary"]["corrected_words"])

# Output: {"projetc": "project", "managment": "management"}

内容处理

HTML 内容会自动使用 BeautifulSoup 进行清理和处理:


# Complex HTML content like this:

html_content = """

<div class="article">

<h1>API Documentation</h1>

<p>Learn how to integrate with our <a href="/api">REST API</a>.</p>

    <img src="/images/api-flow.png" alt="API workflow diagram">

<ul>

<li><strong>Authentication:</strong> Use <code>Bearer tokens

  • Rate limiting: 1000 requests/hour
  • See our code examples for details.
    Method POST
    analytics.track('page_view'); .hidden { display: none; }
    """ # Is automatically converted to clean, searchable plain text: """ API Documentation Learn how to integrate with our REST API. Authentication: Use Bearer tokens Rate limiting: 1000 requests/hour See our code examples for details. Method POST """ # The cleaning process: # 1. Removes all HTML tags (
    ,

    , , , etc.) # 2. Strips out scripts, styles, and non-content elements # 3. Extracts link text while removing href URLs # 4. Normalizes whitespace and line breaks
    
    results = search.search("project update", title_only=True)
    

    高级过滤

    
    accessible_projects = ['PROJ001', 'PROJ002', ...]
    
    filters = {
    
        "project": accessible_projects,     # Multiple values (IN clause)
    
        "owner": current_user,              # Single value (= clause)
    
    }
    
    results = search.search("bug fix", filters=filters)
    

    自动索引处理

    当您注册搜索类时,框架会自动处理索引的构建和维护:

    
    # hooks.py
    
    sqlite_search = ['my_app.search.MyAppSearch']
    

    框架自动完成的工作:

    1. 迁移后索引构建:运行 bench migrate 后自动构建搜索索引

    2. 定期索引验证:每 15 分钟检查一次索引是否存在,如果缺失则重新构建

    3. 实时文档更新:在文档生命周期事件(插入、更新、删除)中自动调用 index_doc()remove_doc(),适用于您在 INDEXABLE_DOCTYPES 中定义的所有文档类型

    手动索引处理

    如果您希望手动控制索引的生命周期,可以通过不在 sqlite_search 钩子中注册搜索类来退出自动索引处理。

    
    from my_app.search import MyAppSearch
    
    def build_index_in_background():
    
        """Manually trigger background index building"""
    
        search = MyAppSearch()
    
        if search.is_search_enabled() and not search.index_exists():
    
            frappe.enqueue("my_app.search.build_index", queue="long")
    
    # hooks.py
    
    scheduler_events = {
    
        # Custom scheduler (if you want different timing)
    
        "daily": ["my_app.search.build_index_if_not_exists"],
    
    }
    

    API 参考

    search(query, title_only=False, filters=None)

    返回格式化结果的主要搜索方法。

    参数:

    • query(字符串):搜索查询文本

    • title_only(布尔值):仅在标题字段中搜索

    • filters(字典):要应用的附加过滤器

    返回值:

    
    {
    
        "results": [
    
            {
    
                "doctype": "Task",
    
                "name": "TASK-001",
    
                "title": "Fix login bug",
    
                "content": "User cannot login after password reset...",
    
                "score": 0.85,
    
                "original_rank": 3, # original bm25 rank
    
                "rank": 1, # modified rank after custom scoring pipeline
    
                # ... other metadata fields
    
            }
    
        ],
    
        "summary": {
    
            "duration": 0.023,
    
            "total_matches": 15,
    
            "returned_matches": 15,
    
            "corrected_words": {"loggin": "login"},
    
            "corrected_query": "Fix login bug",
    
            "title_only": False,
    
            "filtered_matches": 15,
    
            "applied_filters": {"status": ["Open"]}
    
        }
    
    }
    

    build_index()

    从头构建完整的搜索索引。

    index_doc(doctype, docname)

    索引单个文档。

    remove_doc(doctype, docname)

    从索引中移除单个文档。

    is_search_enabled()

    检查搜索是否已启用(覆盖以添加禁用逻辑)。

    index_exists()

    检查搜索索引是否存在。

    get_search_filters()

    必须由子类实现。返回当前用户的过滤器。

    返回值:

    
    {
    
        "field_name": "value",           # Single value
    
        "field_name": ["val1", "val2"],  # Multiple values
    
    }
    

    scoring_function()

    使用 @SQLiteSearch.scoring_function 装饰器将函数标记为评分函数。

    frappe.qb获取查询

    [["status", "in", ["Open", "Pending"]]]

    not in NOT IN {"role": ["not in", ["Guest"]]} [["role", "not in", ["Guest"]]] is IS NULL IS NOT NULL {"customer": ["is", "set"]} {"email": ["is", "not set"]} [["customer", "is", "set"]] [["email", "is", "not set"]] descendants of {"parent_account": ["descendants of", "Assets"]} [["parent_account", "descendants of", "Assets"]] ancestors of {"location": ["ancestors of", "Room 101"]} [["location", "ancestors of", "Room 101"]] not descendants of {"category": ["not descendants of", "Internal"]} [["category", "not descendants of", "Internal"]] not ancestors of {"territory": ["not ancestors of", "West Coast"]} [["territory", "not ancestors of", "West Coast"]] is set is not set IS NOT NULL IS NULL link_fieldname.target_fieldname child_table_fieldname.target_fieldname distinct=True lft rgt filters 'and' 'or' query .run() as_iterator=True as_iterator=True as_dict=True as_list=True frappe.db.unbuffered_cursor() order_by group_by limit offset distinct=True ignore_permissions frappe.qb.get_query ignore_permissions=True ignore_permissions=False ignore_permissions=False if_owner fields filters group_by order_by link_field.target_field child_field.target_field ignore_permissions=False fields filters frappe.PermissionError group_by frappe.PermissionError order_by frappe.PermissionError link_field.target_field child_field.target_field frappe.qb.get_query ignore_permissions=False __CODEBLOCK_156__ __CODEBLOCK_157__ __CODEBLOCK_158__ __CODEBLOCK_159__ __CODEBLOCK_160__ __CODEBLOCK_161__ __CODEBLOCK_162__ __CODEBLOCK_163__ __CODEBLOCK_164__ __CODEBLOCK_165__ __CODEBLOCK_166__ __CODEBLOCK_167__ __CODEBLOCK_168__ __CODEBLOCK_169__ __CODEBLOCK_170__ __CODEBLOCK_171__ __CODEBLOCK_172__ __CODEBLOCK_173__ __CODEBLOCK_174__ __CODEBLOCK_175__ __CODEBLOCK_176__ __CODEBLOCK_177__ __CODEBLOCK_178__ __CODEBLOCK_179__ __CODEBLOCK_180__ __CODEBLOCK_181__ __CODEBLOCK_182__ __CODEBLOCK_183__ __CODEBLOCK_184__ __CODEBLOCK_185__ __CODEBLOCK_186__ __CODEBLOCK_187__ __CODEBLOCK_188__ __CODEBLOCK_189__ __CODEBLOCK_190__ __CODEBLOCK_191__ __CODEBLOCK_192__ __CODEBLOCK_193__ __CODEBLOCK_194__ __CODEBLOCK_195__ __CODEBLOCK_196__ __CODEBLOCK_197__ __CODEBLOCK_198__ __CODEBLOCK_199__ __CODEBLOCK_200__ __CODEBLOCK_201__ __CODEBLOCK_202__ __CODEBLOCK_203__ __CODEBLOCK_204__ __CODEBLOCK_205__ __CODEBLOCK_206__ __CODEBLOCK_207__ __CODEBLOCK_208__ __CODEBLOCK_209__ __CODEBLOCK_210__ __CODEBLOCK_211__ __CODEBLOCK_212__ __CODEBLOCK_213__ __CODEBLOCK_214__ __CODEBLOCK_215__ __CODEBLOCK_216__ __CODEBLOCK_217__ __CODEBLOCK_218__ __CODEBLOCK_219__ __CODEBLOCK_220__ __CODEBLOCK_221__ __CODEBLOCK_222__ __CODEBLOCK_223__ __CODEBLOCK_224__ __CODEBLOCK_225__ __CODEBLOCK_226__ __CODEBLOCK_227__ __CODEBLOCK_228__ __CODEBLOCK_229__ __CODEBLOCK_230__ __CODEBLOCK_231__ __CODEBLOCK_232__ __CODEBLOCK_233__ __CODEBLOCK_234__ __CODEBLOCK_235__ __CODEBLOCK_236__ __CODEBLOCK_237__ __CODEBLOCK_238__ __CODEBLOCK_239__ __CODEBLOCK_240__ __CODEBLOCK_241__ __CODEBLOCK_242__ __CODEBLOCK_243__ __CODEBLOCK_244__ __CODEBLOCK_245__ __CODEBLOCK_246__ __CODEBLOCK_247__ __CODEBLOCK_248__ __CODEBLOCK_249__ __CODEBLOCK_250__ __CODEBLOCK_251__ __CODEBLOCK_252__ __CODEBLOCK_253__ __CODEBLOCK_254__ __CODEBLOCK_255__ __CODEBLOCK_256__ __CODEBLOCK_257__ __CODEBLOCK_258__ __CODEBLOCK_259__ __CODEBLOCK_260__ __CODEBLOCK_261__ __CODEBLOCK_262__ __CODEBLOCK_263__ __CODEBLOCK_264__ __CODEBLOCK_265__ __CODEBLOCK_266__ __CODEBLOCK_267__ __CODEBLOCK_268__ __CODEBLOCK_269__ __CODEBLOCK_270__ __CODEBLOCK_271__ __CODEBLOCK_272__ __CODEBLOCK_273__ __CODEBLOCK_274__ __CODEBLOCK_275__ __CODEBLOCK_276__ __CODEBLOCK_277__ __CODEBLOCK_278__ __CODEBLOCK_279__ __CODEBLOCK_280__ __CODEBLOCK_281__ __CODEBLOCK_282__ __CODEBLOCK_283__ __CODEBLOCK_284__ __CODEBLOCK_285__ __CODEBLOCK_286__ __CODEBLOCK_287__ __CODEBLOCK_288__ __CODEBLOCK_289__ __CODEBLOCK_290__ __CODEBLOCK_291__ __CODEBLOCK_292__ __CODEBLOCK_293__ __CODEBLOCK_294__ __CODEBLOCK_295__ __CODEBLOCK_296__ __CODEBLOCK_297__ __CODEBLOCK_298__ __CODEBLOCK_299__ __CODEBLOCK_300__ __CODEBLOCK_301__ __CODEBLOCK_302__ __CODEBLOCK_303__ __CODEBLOCK_304__ __CODEBLOCK_305__ __CODEBLOCK_306__ __CODEBLOCK_307__ __CODEBLOCK_308__ __CODEBLOCK_309__ __CODEBLOCK_310__ __CODEBLOCK_311__ __CODEBLOCK_312__ __CODEBLOCK_313__ __CODEBLOCK_314__ __CODEBLOCK_315__ __CODEBLOCK_316__ __CODEBLOCK_317__ __CODEBLOCK_318__ __CODEBLOCK_319__ __CODEBLOCK_320__ __CODEBLOCK_321__ __CODEBLOCK_322__ __CODEBLOCK_323__ __CODEBLOCK_324__ __CODEBLOCK_325__ __CODEBLOCK_326__ __CODEBLOCK_327__ __CODEBLOCK_328__ __CODEBLOCK_329__ __CODEBLOCK_330__ __CODEBLOCK_331__ __CODEBLOCK_332__ __CODEBLOCK_333__ __CODEBLOCK_334__ __CODEBLOCK_335__ __CODEBLOCK_336__ __CODEBLOCK_337__ __CODEBLOCK_338__ __CODEBLOCK_339__ __CODEBLOCK_340__ __CODEBLOCK_341__ __CODEBLOCK_342__ __CODEBLOCK_343__ __CODEBLOCK_344__ __CODEBLOCK_345__ __CODEBLOCK_346__ __CODEBLOCK_347__ __CODEBLOCK_348__ __CODEBLOCK_349__ __CODEBLOCK_350__ __CODEBLOCK_351__ __CODEBLOCK_352__ __CODEBLOCK_353__ __CODEBLOCK_354__ __CODEBLOCK_355__ __CODEBLOCK_356__ __CODEBLOCK_357__ __CODEBLOCK_358__ __CODEBLOCK_359__ __CODEBLOCK_360__ __CODEBLOCK_361__ __CODEBLOCK_362__ __CODEBLOCK_363__ __CODEBLOCK_364__ __CODEBLOCK_365__ __CODEBLOCK_366__ __CODEBLOCK_367__ __CODEBLOCK_368__ __CODEBLOCK_369__ __CODEBLOCK_370__ __CODEBLOCK_371__ __CODEBLOCK_372__ __CODEBLOCK_373__ __CODEBLOCK_374__ __CODEBLOCK_375__ __CODEBLOCK_376__ __CODEBLOCK_377__ __CODEBLOCK_378__ __CODEBLOCK_379__ __CODEBLOCK_380__ __CODEBLOCK_381__ __CODEBLOCK_382__ __CODEBLOCK_383__ __CODEBLOCK_384__ __CODEBLOCK_385__ __CODEBLOCK_386__ __CODEBLOCK_387__ __CODEBLOCK_388__ __CODEBLOCK_389__ __CODEBLOCK_390__ __CODEBLOCK_391__ __CODEBLOCK_392__ __CODEBLOCK_393__ __CODEBLOCK_394__ __CODEBLOCK_395__ __CODEBLOCK_396__ __CODEBLOCK_397__ __CODEBLOCK_398__ __CODEBLOCK_399__ __CODEBLOCK_400__ __CODEBLOCK_401__ __CODEBLOCK_402__ __CODEBLOCK_403__ __CODEBLOCK_404__ __CODEBLOCK_405__ __CODEBLOCK_406__ __CODEBLOCK_407__ __CODEBLOCK_408__ __CODEBLOCK_409__ __CODEBLOCK_410__ __CODEBLOCK_411__ __CODEBLOCK_412__ __CODEBLOCK_413__ __CODEBLOCK_414__ __CODEBLOCK_415__ __CODEBLOCK_416__ __CODEBLOCK_417__ __CODEBLOCK_418__ __CODEBLOCK_419__ __CODEBLOCK_420__ __CODEBLOCK_421__ __CODEBLOCK_422__ __CODEBLOCK_423__ __CODEBLOCK_424__ __CODEBLOCK_425__ __CODEBLOCK_426__ __CODEBLOCK_427__ __CODEBLOCK_428__ __CODEBLOCK_429__ __CODEBLOCK_430__ __CODEBLOCK_431__ __CODEBLOCK_432__ __CODEBLOCK_433__ __CODEBLOCK_434__ __CODEBLOCK_435__ __CODEBLOCK_436__ __CODEBLOCK_437__ __CODEBLOCK_438__ __CODEBLOCK_439__ __CODEBLOCK_440__ __CODEBLOCK_441__ __CODEBLOCK_442__ __CODEBLOCK_443__ __CODEBLOCK_444__ __CODEBLOCK_445__ __CODEBLOCK_446__ __CODEBLOCK_447__ __CODEBLOCK_448__ __CODEBLOCK_449__ __CODEBLOCK_450__ __CODEBLOCK_451__ __CODEBLOCK_452__ __CODEBLOCK_453__ __CODEBLOCK_454__ __CODEBLOCK_455__ __CODEBLOCK_456__ __CODEBLOCK_457__ __CODEBLOCK_458__ __CODEBLOCK_459__ __CODEBLOCK_460__ __CODEBLOCK_461__ __CODEBLOCK_462__ __CODEBLOCK_463__ __CODEBLOCK_464__ __CODEBLOCK_465__ __CODEBLOCK_466__ __CODEBLOCK_467__ __CODEBLOCK_468__ __CODEBLOCK_469__ __CODEBLOCK_470__ __CODEBLOCK_471__ __CODEBLOCK_472__ __CODEBLOCK_473__ __CODEBLOCK_474__ __CODEBLOCK_475__ __CODEBLOCK_476__ __CODEBLOCK_477__ __CODEBLOCK_478__ __CODEBLOCK_479__ __CODEBLOCK_480__ __CODEBLOCK_481__ __CODEBLOCK_482__ __CODEBLOCK_483__ __CODEBLOCK_484__ __CODEBLOCK_485__ __CODEBLOCK_486__ __CODEBLOCK_487__ __CODEBLOCK_488__ __CODEBLOCK_489__ __CODEBLOCK_490__ __CODEBLOCK_491__ __CODEBLOCK_492__ __CODEBLOCK_493__ __CODEBLOCK_494__ __CODEBLOCK_495__ __CODEBLOCK_496__ __CODEBLOCK_497__ __CODEBLOCK_498__ __CODEBLOCK_499__ __CODEBLOCK_500__ __CODEBLOCK_501__ __CODEBLOCK_502__ __CODEBLOCK_503__ __CODEBLOCK_504__ __CODEBLOCK_505__ __CODEBLOCK_506__ __CODEBLOCK_507__ __CODEBLOCK_508__ <td style="text-align:left [["status", "in", ["Open", "Pending"]]] not in NOT IN {"role": ["not in", ["Guest"]]} [["role", "not in", ["Guest"]]] is IS NULLIS NOT NULL {"customer": ["is", "set"]}{"email": ["is", "not set"]} [["customer", "is", "set"]][["email", "is", "not set"]] descendants of (嵌套集) {"parent_account": ["descendants of", "Assets"]} [["parent_account", "descendants of", "Assets"]] ancestors of (嵌套集) {"location": ["ancestors of", "Room 101"]} [["location", "ancestors of", "Room 101"]] not descendants of (嵌套集) {"category": ["not descendants of", "Internal"]} [["category", "not descendants of", "Internal"]] not ancestors of (嵌套集) {"territory": ["not ancestors of", "West Coast"]} [["territory", "not ancestors of", "West Coast"]]

    关于 is set / is not set 的说明: 这些用于检查字段是否有值(分别为 IS NOT NULLIS NULL)。

    按链接文档字段筛选

    您可以使用点号表示法根据链接文档中的字段进行筛选:link_fieldname.target_fieldname

    # Get Sales Orders where the linked Customer's territory is 'North America'
    query = frappe.qb.get_query(
        "Sales Order",
        fields=["name", "customer"],
        filters={"customer.territory": "North America"} # Filter on linked field
    )
    north_america_orders = query.run(as_dict=True)
    

    按子表字段筛选

    您可以使用点号表示法根据子表记录中的值筛选父记录:child_table_fieldname.target_fieldname

    # Get Sales Orders that contain 'Item A' in their items table
    # Use distinct=True to ensure each Sales Order appears only once
    query = frappe.qb.get_query(
        "Sales Order",
        fields=["name", "customer"],
        filters={"items.item_code": "Item A"}, # Filter based on child table field
        distinct=True
    )
    orders_with_item_a = query.run(as_dict=True)
    

    重要提示: 当基于子表字段进行筛选时,如果您只需要唯一的父记录,请使用 distinct=True

    嵌套集筛选

    对于树形结构的 DocType(使用 lftrgt 列,如科目、地区、仓库等),您可以使用特殊筛选器:

    # Get all accounts under 'Assets'
    query = frappe.qb.get_query(
        "Account",
        fields=["name"],
        filters={"parent_account": ["descendants of", "Assets"]}
    )
    
    # Get the parent territories of 'West Coast'
    query = frappe.qb.get_query(
        "Territory",
        fields=["name"],
        filters={"parent_territory": ["ancestors of", "West Coast"]}
    )
    

    逻辑运算符(AND/OR)

    对于复杂条件,请将您的 filters 构建为列表,并使用 'and''or' 组合条件。

    # Find users who are enabled AND have first name 'Admin'
    filters_and = [
        ["enabled", "=", 1],
        "and",
        ["first_name", "=", "Admin"],
    ]
    query = frappe.qb.get_query("User", filters=filters_and)
    
    # Find users who have first name 'Admin' OR 'Guest'
    filters_or = [
        ["first_name", "=", "Admin"],
        "or",
        ["first_name", "=", "Guest"],
    ]
    query = frappe.qb.get_query("User", filters=filters_or)
    
    # Combine AND and OR (use nested lists for grouping)
    # Find users who are enabled AND (have first name 'Admin' OR 'Guest')
    filters_nested = [
        ["enabled", "=", 1],
        "and",
        [
            ["first_name", "=", "Admin"],
            "or",
            ["first_name", "=", "Guest"],
        ]
    ]
    query = frappe.qb.get_query("User", filters=filters_nested)
    

    查询执行

    基本执行

    一旦您获得了 query 对象,请使用 .run() 执行它:

    # Returns a list of tuples by default
    results = query.run()
    
    # Returns a list of dictionaries
    results = query.run(as_dict=True)
    
    # Returns a list of lists
    results = query.run(as_list=True)
    
    # If selecting a single field, returns a flat list of values
    results = query.run(pluck=True)
    
    # Print the generated SQL query and execution time
    results = query.run(debug=True)
    

    获取 SQL 字符串

    您可以在不执行的情况下获取生成的 SQL 字符串:

    # Get the SQL string with values directly substituted (for debugging)
    sql_string = query.get_sql()
    print(sql_string)
    # Example Output: SELECT `name`, `email` FROM `tabUser` WHERE `first_name`='Admin'
    

    对大型数据集使用迭代器

    处理大型数据集时,请使用 as_iterator=True 逐行处理结果,而无需将所有内容加载到内存中:

    # Process a large number of tasks without loading all into memory
    query = frappe.qb.get_query(
        "Task",
        fields=["name", "subject", "status"],
        filters={"status": "Open"}
    )
    
    # Use unbuffered_cursor for optimal memory usage with the iterator
    with frappe.db.unbuffered_cursor():
        task_iterator = query.run(as_iterator=True, as_dict=True)
    
        processed_count = 0
        for task in task_iterator:
            # Process each task dictionary one by one
            print(f"Processing Task: {task['name']} - {task['subject']}")
            processed_count += 1
            if processed_count % 1000 == 0:
                print(f"Processed {processed_count} tasks...")
    

    要求:

    • 您必须将 as_iterator=Trueas_dict=Trueas_list=True 一起使用
    • 为获得最佳内存效率,请在 frappe.db.unbuffered_cursor() 上下文管理器中使用

    排序、分组和分页

    排序结果

    使用 order_by 参数对结果进行排序:

    # Order users by creation date, ascending
    query = frappe.qb.get_query("User", fields=["name", "creation"], order_by="creation asc")
    
    # Order by multiple fields
    query = frappe.qb.get_query(
        "Sales Invoice",
        fields=["name", "customer", "grand_total"],
        order_by="customer asc, grand_total desc"
    )
    

    分组结果

    使用 group_by 进行聚合:

    # Count invoices per customer
    query = frappe.qb.get_query(
        "Sales Invoice",
        fields=["customer", {"COUNT": "'*'", "as": "invoice_count"}],
        filters={"docstatus": 1},
        group_by="customer"
    )
    results = query.run(as_dict=True)
    # results: [{'customer': 'Cust A', 'invoice_count': 5}, {'customer': 'Cust B', 'invoice_count': 3}, ...]
    

    分页

    使用 limitoffset 进行分页:

    # Get the first 10 users
    query = frappe.qb.get_query("User", limit=10)
    
    # Get the next 10 users (page 2)
    query = frappe.qb.get_query("User", limit=10, offset=10)
    

    去重结果

    使用 distinct=True 获取唯一行:

    # Get distinct customers from submitted Sales Invoices
    query = frappe.qb.get_query(
        "Sales Invoice",
        fields=["customer"],
        filters={"docstatus": 1},
        distinct=True
    )
    

    权限

    ignore_permissions 标志

    默认情况下,frappe.qb.get_query 忽略权限(ignore_permissions=True)。要强制执行权限,请设置 ignore_permissions=False

    # This query bypasses all permission checks (default behavior)
    query_ignore = frappe.qb.get_query("DocType", fields=["name"], filters={"istable": 1})
    
    # This query enforces standard Frappe permissions for the current user
    query_enforce = frappe.qb.get_query(
        "DocType",
        fields=["name"],
        filters={"istable": 1},
        ignore_permissions=False # Explicitly enable permission checks
    )
    
    try:
        results = query_enforce.run()
    except frappe.PermissionError:
        print("User does not have permission to read DocType!")
    

    权限的应用方式

    ignore_permissions=False 时:

    1. 角色权限: 根据用户的角色检查其是否具有“读取”或“选择”权限。
    2. 用户权限: 应用为 DocType 和链接的 DocType 定义的用户权限(允许/限制)。
    3. 共享: 包含明确共享给用户的文档。
    4. 所有者约束: 如果角色权限仅授予 if_owner 访问权限,则查询会将结果限制为用户拥有的文档。
    5. 权限查询条件: 应用通过 Hooks 或服务器脚本定义的条件。
    6. 字段级安全: 筛选所选的 fields,如果用户没有权限级别访问权限,则不允许使用 filtersgroup_byorder_by 中使用的字段。同时检查 link_field.target_fieldchild_field.target_field 表示法中的字段。

    字段级安全

    ignore_permissions=False 时:

    • fields 仅包含用户最大允许权限级别下可访问的字段。请求不可访问的字段将静默移除该字段的选择。
    • filters 筛选仅允许在用户有权访问的字段上进行。尝试筛选不可访问的字段将引发 frappe.PermissionError
    • group_by 分组仅允许在用户有权访问的字段上进行。尝试按不可访问的字段分组将引发 frappe.PermissionError
    • order_by 仅允许对用户有权限访问的字段进行排序。尝试按无权限访问的字段排序将引发 frappe.PermissionError
    • 链接表和子表字段: 在上述任何子句中使用 link_field.target_fieldchild_field.target_field 表示法时,系统会同时检查链接/子字段本身的权限,以及链接/子 DocType 中目标字段的权限。
    # Assume 'published' field in Blog Post has permlevel 1
    # User '[email protected]' only has permlevel 0 access
    
    # This works, but 'published' field is silently removed from results
    query = frappe.qb.get_query(
        "Blog Post",
        fields=["name", "title", "published"], # 'published' requested but inaccessible
        ignore_permissions=False,
        user="[email protected]"
    )
    # result will contain 'name' and 'title', but NOT 'published'
    
    # This FAILS because filtering on 'published' is not allowed for this user
    try:
        query = frappe.qb.get_query(
            "Blog Post",
            fields=["name"],
            filters={"published": 1}, # Filtering on restricted field
            ignore_permissions=False,
            user="[email protected]"
        )
        query.run()
    except frappe.PermissionError as e:
        print(f"Permission error: {e}")
    

    指定用户和父级上下文

    # Check permissions for a specific user
    query = frappe.qb.get_query(
        "Task",
        ignore_permissions=False,
        user="[email protected]" # Check permissions for this user
    )
    
    # Provide parent context for child DocTypes
    query = frappe.qb.get_query(
        "Sales Order Item",
        fields=["item_code", "qty"],
        filters={"parent": "SO-00001"},
        ignore_permissions=False,
        parent_doctype="Sales Order" # Specify parent context for permission checks
    )
    

    高级功能

    使用 Pypika 对象

    对于涉及子查询、高级条件或字典语法中不可用函数的复杂场景,您可以直接使用 Pypika 对象。

    字段中的 Pypika 对象

    from frappe.query_builder import Field, functions, Query
    
    # Define Pypika objects
    user_table = frappe.qb.DocType("User")
    todo_table = frappe.qb.DocType("ToDo")
    
    # Build a subquery to count open ToDos for each user
    open_todo_subquery = (
        Query.from_(todo_table)
        .select(functions.Count("*"))
        .where(todo_table.owner == user_table.name) # Correlated subquery
        .where(todo_table.status == "Open")
    ).as_("open_todos_count")
    
    # Use Pypika objects in fields
    query = frappe.qb.get_query(
        "User",
        fields=[
            user_table.name,
            user_table.email,
            open_todo_subquery # Using the subquery object
        ],
        filters={"user_type": "System User"}
    )
    users_with_counts = query.run(as_dict=True)
    

    过滤器中的 Pypika 对象

    from frappe.query_builder import Field, functions
    
    # Define Pypika objects
    task_table = frappe.qb.DocType("Task")
    modified_field = task_table.modified
    creation_field = task_table.creation
    subject_field = task_table.subject
    status_field = task_table.status
    
    # Build complex criterion
    complex_filter = (
        (modified_field > creation_field) & (functions.Length(subject_field) > 10)
    ) | (status_field == "Cancelled")
    
    # Use the Criterion object in filters
    query = frappe.qb.get_query(
        "Task",
        fields=["name", "subject", "status", "creation", "modified"],
        filters=complex_filter
    )
    results = query.run(as_dict=True)
    

    记录锁定

    用于数据库事务中,防止其他事务修改特定行:

    基本锁定

    # Lock specific Stock Ledger Entries
    query = frappe.qb.get_query(
        "Stock Ledger Entry",
        fields=["name", "qty_after_transaction"],
        filters={"item_code": "ITEM001", "warehouse": "WH001"},
        for_update=True # Adds FOR UPDATE clause, will wait if rows are locked
    )
    entries = query.run(as_dict=True)
    

    跳过已锁定行

    # Skip rows that are already locked by another transaction
    query = frappe.qb.get_query(
        "ToDo",
        fields=["name", "description"],
        filters={"status": "Pending"},
        limit=5,
        order_by="creation asc",
        for_update=True,
        skip_locked=True # Skip locked rows
    )
    available_tasks = query.run(as_dict=True)
    

    非阻塞锁定尝试

    # Fail immediately if rows are already locked
    try:
        query = frappe.qb.get_query(
            "System Settings",
            fields=["name"],
            filters={"name": "System Settings"},
            for_update=True,
            wait=False # Don't wait for locks
        )
        settings = query.run(as_dict=True)
    except Exception as e:
        print(f"Could not acquire lock immediately: {e}")
    

    安全注意事项

    frappe.qb.get_query 在设计时充分考虑了安全性:

    • 字段验证: 字段名称会按照严格的模式进行验证,以防止 SQL 注入。
    • 参数化: 过滤器值由数据库驱动程序进行参数化处理。
    • 权限执行: 使用 ignore_permissions=False 可充分利用 Frappe 强大的权限系统。

    请始终确保,如果用于构建过滤器键或字段名称的任何动态值来自不受信任的来源,都经过适当的清理。始终依赖将用户输入作为过滤器值传递。

    由 Claude Sonnet 4 编写。经人工审核。

    查询构建器

    frappe.qb 是一个基于 PyPika 构建的查询构建器,用于为跨数据库查询提供统一接口。

    在开发应用程序时,您经常需要从数据库中检索特定数据。一种方法是使用 frappe.db.sql 并编写原始 SQL 查询。

    可能类似于这样

    result = frappe.db.sql(
     f"""
     SELECT `path`,
     COUNT(*) as count,
     COUNT(CASE WHEN CAST(`is_unique` as Integer) = 1 THEN 1 END) as unique_count
     FROM `tabWeb Page View`
     WHERE `creation` BETWEEN {some_date} AND {some_later_date}
     """
    )
    

    查询构建器 API 通过提供简单的 Pythonic API 来构建 SQL 查询,同时不限制手写 SQL 的灵活性,从而使这一过程更加容易。

    同样的查询在查询构建器中看起来会是这样

    import frappe
    from frappe.query_builder import DocType
    from frappe.query_builder.functions import Count
    from pypika.terms import Case
    
    WebPageView = DocType("Web Page View") # you can also use frappe.qb.DocType to bypass an import
    
    count_all = Count('*').as_("count")
    case = Case().when(WebPageView.is_unique == "1", "1")
    count_is_unique = Count(case).as_("unique_count")
    
    result = (
     frappe.qb.from_(WebPageView)
     .select(WebPageView.path, count_all, count_is_unique)
     .where(Web_Page_View.creation[some_date:some_later_date])
    ).run()
    

    frappe.qb

    返回一个 Pypika 查询对象,用于构建查询。使用此对象构建的查询将是 pypika.dialects 中的类型,并带有一些 Frappe 的增强功能。它的一些方法包括:

    frappe.qb.from_(doctype)

    允许您构建一个 from 查询来选择数据。

    选择查询

    query = frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone')
    

    构建的 SQL 查询为

    SELECT `id`,`fname`,`lname`,`phone` FROM `tabCustomer`
    

    一个复杂的 Select 示例

    customers = frappe.qb.DocType('Customer')
    q = (
     frappe.qb.from_(customers)
     .select(customers.id, customers.fname,customers.lname, customers.phone)
     .where((customers.fname == 'Max') | (customers.id.like('RA%')) )
     .where(customers.lname == 'Mustermann')
    )
    

    构建的 SQL 查询为

    SELECT `id`,`fname`,`lname`,`phone` FROM `tabCustomer` WHERE (`fname`='Max' OR `id` LIKE 'RA%') AND `lname`='Mustermann'
    

    一些值得注意的事项

    • 我们创建了一个 customers 变量来引用查询中的表。
    • Select 可以接受任意数量的参数,选择各种字段。
    • 可以使用 ‘|’(管道符)或 ‘&’(与符号)运算符来表示 ‘OR’ 或 ‘AND’。
    • 链式调用 where() 方法默认会追加 ‘AND’。

    您可以在 Pypika 仓库中阅读有关其他函数的更多信息。

    frappe.qb.Doctype(name_of_table)

    返回一个 PyPika 表对象,可在其他地方使用。如有必要,它会自动添加 ‘tab’ 前缀。

    frappe.qb.Table(name_of_table)

    frappe.qb.DocType 功能相同,但不会追加 ‘tab’ 前缀。它旨在用于像 ‘__Auth’ 这样的表。

    注意:只有在您清楚自己在做什么的情况下才应使用此功能。

    frappe.qb.Field(name_of_coloum)

    返回一个 PyPika 字段对象,代表一个列。它们通常用于将列与值进行比较。

    一个例子是

    lname = frappe.qb.Field("lname")
    q = frapppe.qb.from_("customers").select("*").where(lname == 'Mustermann')
    

    执行查询

    使用 frappe.qb 命名空间构建的查询是 PyPika 对象。它们必须转换为字符串对象,以便您的数据库管理系统能够识别它们。

    要检查您的查询对象如何转换,您可以使用 str 进行类型转换,或使用它们自带的 .get_sql 方法。

    query = frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone')
    
    str(query)
    # SELECT "id","fname","lname","phone" FROM "tabCustomer"
    
    query.get_sql()
    # SELECT "id","fname","lname","phone" FROM "tabCustomer"
    
    str(query) == query.get_sql()
    # True
    

    Walk 方法

    所有通过 frappe.qb 构建的查询默认都是参数化的。所有输入字段、原始值和函数都被分离为命名参数,并以字典形式发送到数据库。参数化是为了净化查询,防止 SQL 注入。

    您可以使用 walk 方法查看哪些部分被参数化了。它返回参数化的查询和相应的字典。

    doctype = frappe.qb.DocType("DocType")
    
    frappe.qb.from_(doctype).select('*').where(doctype.name == "somename").walk()
    # ('SELECT * FROM `tabDocType` WHERE `name`=%(param1)s', {'param1': 'somename'})
    

    Run 方法

    这是执行查询最推荐的方法。每个有效的查询都有 run 方法,您可以使用它来执行查询。

    frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone').run()
    

    run 方法接受 kwargs,这些参数将在查询执行时传递。您可以通过 run 方法传递 frappe.db.sql 中可用的任何选项。

    要对查询进行调试,或以 List[Dict] 的形式获取结果,您可以分别使用以下方法:

    In [7]: frappe.qb.from_('ToDo').select('name').run(debug=True)
    SELECT "name" FROM "tabToDo"
    Execution time: 0.0 sec
    Out[7]: [('8d765f73a2',)]
    
    In [8]: frappe.qb.from_('ToDo').select('name').run(as_dict=True)
    Out[8]: [{'name': '8d765f73a2'}]
    

    run 方法在内部调用更底层的 frappe.db.sql API。

    frappe.db.sql

    您也可以选择直接将查询对象传递给 frappe.db.sql。但这会忽略查询的权限和参数化。

    query = frappe.qb.from_('Customer').select('id', 'fname', 'lname', 'phone')
    frappe.db.sql(query)
    

    frappe.query_builder.functions

    此模块提供了您在构建查询时可能需要的标准函数,例如 Count()Sum().

    连接和子查询

    您可以查看 pypika 文档来了解如何连接表和添加子查询。请使用 frappe.qb.DocType 代替 Table

    示例:

    HasRole = frappe.qb.DocType('Has Role')
    CustomRole = frappe.qb.DocType('Custom Role')
    
    query = (frappe.qb.from_(HasRole)
     .inner_join(CustomRole)
     .on(CustomRole.name == HasRole.parent)
     .select(CustomRole.page, HasRole.parent, HasRole.role))
    

    简单函数

    假设您想计算 Notes 表中的所有条目。您可以这样做

    from frappe.query_builder.functions import Count
    
    Notes = frappe.qb.DocType("Notes")
    count_pages = Count(Notes.content).as_("Pages")
    
    result = frappe.qb.from_(Notes).select(count_pages).run(as_dict=True)
    

    JSON 函数

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

    这些辅助函数通过在内部映射到正确的 SQL 方言,使 JSON 查询能够在 MariaDB 和 Postgres 之间移植。

    使用场景:

    1. 按路径读取 JSON 对象值
    2. 读取标量/文本值以进行过滤
    3. 检查 JSON 对象/数组是否包含某个值

    可用的辅助函数:

    1. JSONExtract(field, path)
    2. JSONValue(field, path)
    3. JSONContains(target, candidate)

    示例:JSON 对象字段

    import frappe
    from frappe.query_builder.functions import JSONExtract, JSONValue
    
    CustomerProfile = frappe.qb.DocType("Customer Profile")
    
    # preferences_json:
    # {"notifications": {"email": true, "sms": false}, "language": "en"}
    
    query = (
        frappe.qb.from_(CustomerProfile)
        .select(
            CustomerProfile.customer_name,
            JSONValue(CustomerProfile.preferences_json, "$.language").as_("preferred_language"),
            JSONValue(CustomerProfile.preferences_json, "$.notifications.email").as_("email_notifications"),
        )
        .where(JSONValue(CustomerProfile.preferences_json, "$.notifications.email") == "true")
    )
    

    示例:JSON 列表字段

    import frappe
    from frappe.query_builder.functions import JSONContains, JSONExtract
    
    SalesOrder = frappe.qb.DocType("Sales Order")
    
    # applied_discounts_json:
    # {"codes": ["WELCOME10", "FREESHIP", "VIP"]}
    
    query = (
        frappe.qb.from_(SalesOrder)
        .select(SalesOrder.name, SalesOrder.customer)
        .where(JSONContains(JSONExtract(SalesOrder.applied_discounts_json, "$.codes"), "FREESHIP"))
    )
    

    自定义函数

    frappe.query_builder.functionspypika.functions 的超集,因此它拥有所有 PyPika 函数以及我们创建的一些自定义函数。您可以通过从 PyPika 导入 CustomFunction 类来创建自定义函数。

    DateDiff 函数的一个实现

    from pypika import CustomFunction
    
    customers = Tables('Customer')
    DateDiff = CustomFunction('DATE_DIFF', ['interval', 'start_date', 'end_date'])
    
    q = Query.from_(customers).select(
     DateDiff('day', customers.created_date, customers.updated_date)
    )
    

    如果我们打印 q,我们会得到

    SELECT DATE_DIFF('day',"created_date","updated_date") FROM "Customer"
    

    请注意我们如何指定参数和实际的 SQL 文本。确切的格式可能不适用于更复杂的函数。高级部分涵盖了更复杂的方法。

    常量列

    ConstantColumn 是一个用于定义具有常量值的伪列的类。

    from frappe.query_builder.custom import ConstantColumn
    
    frappe.qb.from_("DocType").select("name", ConstantColumn("john").as_("user"))
    # SELECT `name`,'john' `user` FROM `tabDocType`
    

    这里我们定义了一个值为“john”的列 user。

    高级

    特殊函数

    其中一个这样的函数是 Match Against。它之所以特殊,是因为它有一个链式的 against 参数。要实现类似的功能,你需要继承 PyPika 的 DistinctOptionFunction 类。

    当前的 MATCH 类看起来像这样

    
    from pypika.functions import DistinctOptionFunction
    from pypika.utils import builder
    
    class MATCH(DistinctOptionFunction):
     def __init__(self, column: str, *args:
     super(MATCH, self)._init_(" MATCH", column, *args)
     self._Against = False
    
     def get_function_sql(self, **kwargs):
     s = super(DistinctOptionFunction, self).get_function_sql(**kwargs)
    
     if self._Against:
     return f"{s} AGAINST (f'+{self._Against}*') IN BOOLEAN MODE)"
     return s
    
     @builder
     def Against(self, text: str):
     self._Against = text
    
    • __init__() 方法的工作方式类似于上面的 CustomFunction 类。你需要列出所有参数和 SQL 文本。
    • Against() 方法仅存储一个值,该值将在 get_function_sql() 中使用
    • 它还有 @builder 包装器。简而言之,它通过复制对象使这些函数可以链式调用。
    • 我们包装了 get_function_sql() 方法,这使我们能够追加 Against 所需的 SQL 文本。
    • 这可以进一步扩展以使用任意数量的其他链。

    在使用中,Match 类看起来像这样

    from frappe.query_builder.functions import Match
    
    match = Match("Coloum name").Against("Some_text_match")
    # MATCH('Coloum name') AGAINST ('+Some_text_match*' IN BOOLEAN MODE)
    

    工具

    ImportMapper(dict)

    在极少数情况下,对于不同的 SQL 方言,你有不同的函数,但它们执行相同的操作,你可以使用 ImportMapper 工具。它根据 SQL 方言映射函数,因此一个查询可以在不同的 SQL 方言中工作。

    它接受一个将函数映射到数据库的字典。

    例如,GroupConat 的映射看起来像这样

    
    from frappe.query_builder.utils import ImportMapper, db_type_is
    from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG
    
    GroupConcat = ImportMapper(
     {
     db_type_is.MARIADB: GROUP_CONCAT,
     db_type_is.POSTGRES: STRING_AGG
     }
    )