物料管理

版本 14 中的新功能

包(Package)是轻量级应用,您可以直接从框架界面(Framework UI)构建。包与应用程序非常相似,是模块定义(Module Defs)的集合。

您可以直接从框架界面(无需使用命令行)通过 DocTypes、服务器脚本(Server Scripts)、网页(Web Page)等来创建应用。高质量的应用可以直接从框架中构建。这些应用可以打包成一个“包”,然后导入到其他站点。

要将模块添加到包中,您需要设置其“包”属性。此功能适用于“自定义”模块定义。

创建包

您可以通过创建包对象来创建新包。您可以设置 README 和许可证(LICENSE)。包的设计方式使其可以推送到 git 仓库。

创建包后,您可以将自定义模块定义添加到该包中。

制作包发布版本

包发布会将所有包模块像应用一样导出到 [bench]/sites/[site]/packages 文件夹中,然后将该文件夹打包成 gzip 压缩的 tar 文件 [package]-[version].tar.gz。

然后您可以下载该包发布版本并将其导入到另一个站点。

导入包

您可以创建一个包导入,并将导出的包附加到新站点上。如果您勾选“激活”,该包将被解压到 [bench]/sites/[sitename]/packages 文件夹中(现有文件将被覆盖)。

然后系统会像应用迁移一样将此包迁移到数据库中。除非勾选了“强制”选项,否则未更改的文件将被跳过。

系统会创建包含输出信息的日志。

从链接文档类型更新字段

ERPNext 允许你通过使用 从以下位置获取(Fetch From) 属性,将链接文档中的值提取到另一个字段中。当一个字段应自动显示所选链接记录中的信息时,此功能非常有用。

例如,当用户选择一个 客户(Customer) 时,你可能希望自动显示该客户的区域、税号、客户组或客户主数据中的其他字段。无需编写客户端脚本,你可以通过字段设置中的 从以下位置获取(Fetch From) 来配置此功能。

https://www.youtube.com/watch?v=78Yp_OdXnBI

“从以下位置获取”的工作原理

“从以下位置获取”功能与 链接(Link) 字段配合使用。

其格式为:

link_fieldname.field_to_fetch

其中:

  • link_fieldname 是当前 DocType 中链接字段的字段名。
  • field_to_fetch 是链接的 DocType 中的字段名。

当用户在链接字段中选择一个值时,ERPNext 会读取所选的链接文档,并将指定的字段值复制到当前字段中。

示例

假设你在 销售订单(Sales Order) 上有一个名为 客户区域(Customer Territory) 的自定义字段,并且你希望它自动从所选客户中获取区域信息。

在这种情况下:

  • 销售订单上的链接字段是 customer。
  • 要从客户中获取的字段是 territory。
  • “从以下位置获取”的值应为:
customer.territory

当用户在销售订单中选择一个客户时,ERPNext 会获取该客户的区域,并将其填入“客户区域”字段中。

配置“从以下位置获取”的步骤

  1. 转到 自定义表单(Customize Form)。
  2. 选择要显示获取值的 DocType。
  3. 添加或选择应显示获取值的目标字段。
  4. 在字段设置中,使用以下格式输入 从以下位置获取(Fetch From) 的值:
link_fieldname.field_to_fetch
  1. 保存自定义设置。
  2. 重新加载表单,并通过选择链接文档进行测试。

另一个示例

如果你希望从所选供应商中获取 供应商组(Supplier Group) 到采购订单上的自定义字段中:

  • 链接字段:supplier
  • 要获取的字段:supplier_group
  • “从以下位置获取”的值:
supplier.supplier_group

当选择供应商时,ERPNext 会将供应商组获取到目标字段中。

重要说明

  • 仅当源字段在链接的 DocType 上可用时,“从以下位置获取”功能才有效。
  • 请使用 字段名(fieldname),而不是字段标签。例如,使用 customer_group,而不是 Customer Group。
  • 目标字段应具有兼容的字段类型。例如,将文本获取到数据(Data)字段,将日期获取到日期(Date)字段,并在适当时将链接记录获取到链接(Link)字段。
  • “从以下位置获取”最适合简单的单向获取。如果该值需要复杂的条件、计算或多个依赖更新,请改用客户端脚本。
  • 如果链接文档中的值稍后发生更改,现有文档可能不会自动更新,除非链接字段被更改,或者根据表单行为刷新并保存文档。

常见错误

  • 使用标签而不是字段名: customer.Customer Group 将不起作用。请使用 customer.customer_group。
  • 使用了错误的链接字段: 第一部分必须是当前表单上链接字段的字段名。
  • 从不存在的字段获取: 请检查链接的 DocType 以确认确切的字段名。
  • 期望复杂的逻辑: “从以下位置获取”只是复制一个值。它不能替代业务逻辑或验证。

何时使用“从以下位置获取”

在以下情况下使用“从以下位置获取”:

  • 一个字段应显示来自所选客户、供应商、物料、员工、项目或其他链接记录的值。
  • 该值是信息性的,并跟随所选的链接文档。
  • 你希望进行无代码配置,而不是编写客户端脚本。
  • 获取规则很简单,不需要条件或计算。
  • 自定义表单(Customize Form)
  • 自定义字段(Custom Field)
  • 客户端脚本(Client Script)
  • DocType
  • 链接字段(Link Field)

自定义地理位置/地图字段

如果你想在 DocType 表单中显示地图,可以添加一个类型为“地理位置”的字段。默认情况下,它看起来像这样:

自定义此字段的方法有很多种。我们可以更改全局设置,例如默认位置和缩放级别。我们还可以自定义特定地图上各个对象的渲染方式。

更改地图默认设置

注意:这些更改需要自定义应用,并将应用于系统中的所有地图。

默认情况下,它会显示一张以孟买为中心的城市地图。目前尚无法通过图形界面进行配置。但是,使用自定义应用,我们可以更改地图类型、起始位置、缩放级别等。

首先,创建一个文件 $MY_APP/$MY_APP/public/js/map_defaults.js。在你的 $MY_APP/$MY_APP/hooks.py 中,将 JS 文件添加到 app_include_js 钩子中:

app_include_js = [
    "/assets/$MY_APP/js/map_defaults.js"
    # ...
]

现在,frappe 将为所有后台视图(即已登录的系统用户)加载此文件。

在 map_defaults.js 中,我们添加以下代码行:

// Make sure we have a dictionary to add our custom settings
const map_settings = frappe.provide("frappe.utils.map_defaults");

// Center and zoomlevel can be copied from the URL of
// the map view at openstreetmap.org.

// New default location (middle of germany).
map_settings.center = [51.57, 9.866];
// new zoomlevel: see the whole country, not just a single city
map_settings.zoom = 6;

// Use a different map: satellite instead of streets
// Examples can be found at https://leaflet-extras.github.io/leaflet-providers/preview/
map_settings.tiles = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}";
map_settings.attribution = "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community";

重启你的 bench 并重新加载浏览器窗口。现在你应该能看到自定义后的地图了:

更改特定地图的渲染方式

为了更改特定地图的外观和风格,你可以覆盖地理位置字段的控制器方法。这可以通过客户端脚本或自定义应用中的钩子来完成。

默认情况下,地图上绘制的所有内容都是蓝色的。让我们覆盖 on_each_feature 方法,使所有多边形变为红色:

frappe.ui.form.on("My Doctype", {
    setup(frm) {
        frm.fields_dict.MY_MAP_FIELD.on_each_feature = function(feature, layer) {
            if (feature.geometry.type == "Polygon") {
                layer.setStyle({color: "red"});
            }
        };
    }
});

结果如下所示:

其他可以覆盖的方法包括 point_to_layer 和 set_style。有关这些方法的更多信息,请参阅 Leaflet 文档。

缓存

Frappe 框架内置的电池之一就是使用 Redis 进行缓存。Redis 是一种快速、简单易用的内存键值存储。将 Redis 与 Frappe 框架结合使用,可以加速重复的长耗时计算,或者避免对不常变化的数据进行数据库查询。

Redis 由 bench 作为独立服务启动,每个 Frappe Web/后台工作进程都可以使用 frappe.cache 连接到它。

注意:在旧版本的 Frappe 中,你可能需要使用 frappe.cache() 而不是 frappe.cache 来访问 Redis 连接。

Redis 数据类型与用法

Redis 支持多种数据类型,但这里我们只介绍几种最常用的数据类型:

  1. 字符串(Strings) 是 Redis 中最流行、最常用的数据类型。它们以键值对的形式存储。
In [1]: frappe.cache.set_value("key", "value")

In [2]: frappe.cache.get_value("key")
Out[2]: 'value'
  1. 哈希(Hashes) 用于表示复杂的对象,其字段可以单独更新,而无需发送整个对象。你可以将哈希想象成存储在 Redis 上的字典。
# Get fields separately
In [1]: frappe.cache.hset("user|admin", "name", "Admin")
In [2]: frappe.cache.hset("user|admin", "email", "[email protected]")

# Get single field value
In [3]: frappe.cache.hget("user|admin", "name")
Out[3]: 'Admin'

# Or retrieve all fields at once.
In [4]: frappe.cache.hgetall("user|admin")
Out[4]: {'name': 'Admin', 'email': '[email protected]'}

缓存文档

Frappe 有一个内置函数,用于从缓存而非数据库中获取文档。

从缓存中获取文档通常更快,因此当文档不经常变化时,你应该使用它们。一个常见的用途是获取用户配置的设置。

system_settings = frappe.get_cached_doc("System Settings")

缓存文档会通过“尽力而为”的缓存失效策略自动清除。

每当 Frappe 的 ORM 使用 doc.save 或 frappe.db.set_value 遇到更改时,它都会清除相关文档的缓存。但是,如果对数据库执行了原始查询,则无法实现此操作。

注意:可以使用 frappe.clear_document_cache(doctype, name) 手动进行缓存失效。

实现自定义缓存

当你处理一个耗时很长的昂贵计算,且对于相同的输入,其结果是确定性的,那么缓存输出可能是有意义的。

让我们尝试在这个运行缓慢的示例函数中实现一个自定义缓存。

def slow_add(a, b):
    import time; time.sleep(1) # Fake workload
    return a + b

实现自定义缓存最重要的部分是生成一个唯一的键。在此示例中,缓存值的结果依赖于两个输入变量,因此它们应该是键的一部分。

def slow_add(a, b):
    key = f"slow_addition|{a}+{b}" # unique key representing this computation

    # If this key exists in cache, then return value
    if cached_value := frappe.cache.get_value(key):
        return cached_value

    import time; time.sleep(1) # Fake workload
    result = a + b

    # Set the computed value in cache so next time we dont have to do the work
    frappe.cache.set_value(key, result)

    return result

缓存失效

建议采用两种策略来避免缓存过期问题:

  1. 在设置缓存值时,设置较短的 TTL(生存时间)。
# This cached value will automatically expire in one hour
frappe.cache.set_value(key, result, expires_in_sec=60*60)
  1. 当缓存值被修改时,手动清除缓存。
frappe.cache.delete_value(key) # `frappe.cache.hdel` if using hashes.

@redis_cache 装饰器

Frappe 提供了一个装饰器,用于自动缓存函数调用的结果。

你可以使用它快速在任何可能缓慢的现有函数之上实现缓存。

In [1]: def slow_function(a, b):
   ...:     import time; time.sleep(1)  # fake expensive computation
   ...:     return a + b
   ...:

In [2]: # This takes 1 second to execute every time.
   ...: %time slow_function(40, 2)
   ...:
Wall time: 1 s
Out[2]: 42

In [3]: %time slow_function(40, 2)
Wall time: 1 s
Out[3]: 42

In [4]: from frappe.utils.caching import redis_cache
   ...:

In [5]: @redis_cache
   ...: def slow_function(a, b):
   ...:     import time; time.sleep(1)  # fake expensive computation
   ...:     return a + b
   ...:

In [6]: # Now first call takes 1 second, but all subsequent calls return instantly.
   ...: %time slow_function(40, 2)
   ...:
Wall time: 1 s
Out[6]: 42

In [7]: %time slow_function(40, 2)
   ...:
Wall time: 897 µs
Out[7]: 42

缓存失效

有两种方法可以使 @redis_cache 中的缓存值失效。

  1. 设置适当的过期时间(以秒为单位的 TTL),以便缓存值在一段时间后自动失效。例如:@redis_cache(ttl=60) 将导致缓存值在 60 秒后过期。
  2. 手动清除缓存。这通过调用函数的 clear_cache 方法来完成。
from frappe.utils.caching import redis_cache

@redis_cache
def slow_function(...):
    ...

def invalidate_cache():
    slow_function.clear_cache()

Frappe 的 Redis 设置

Bench 默认会设置 Redis。你可以在 {bench}/config/ 目录中找到 Redis 配置。

Bench 还会配置 Procfile 和 supervisor 配置文件,以便在 bench 启动时启动 Redis 服务器。

redis_cache: redis-server config/redis_cache.conf

示例配置如下所示:

dbfilename redis_cache.rdb
dir /home/user/benches/develop/config/pids
pidfile /home/user/benches/develop/config/pids/redis_cache.pid
bind 127.0.0.1
port 13000
maxmemory 737mb
maxmemory-policy allkeys-lru
appendonly no

save ""

你可以修改此配置中的 maxmemory 来增加分配给缓存的最大内存。我们建议不要修改其他任何内容。

请参阅官方配置文档以了解更多信息:

实现细节

多租户

frappe.cache 在内部会按站点上下文为键添加前缀。因此,在同一 bench 上的两个不同站点调用 frappe.cache.set_value("key") 将为每个站点创建两个独立的条目。

要查看实现细节,请参阅 frappe.cache.make_key 函数。

复杂类型

Frappe 使用 pickle 模块将文档等复杂对象序列化为字节。因此,使用 frappe.cache 时,你无需担心值的序列化/反序列化问题。

在此处阅读更多关于 pickle 的信息:

客户端缓存

Frappe 在 frappe.local.cache 内部的 Redis 缓存之上实现了客户端缓存,以避免对 Redis 的重复调用。

在同一请求/任务中对 Redis 的任何重复调用都将从客户端缓存返回数据,而不是再次调用 Redis。

RedisWrapper

所有与 Frappe 相关的更改都是通过包装默认的 Redis 客户端并扩展其方法来实现的。你可以在 frappe.utils.redis_wrapper 模块中找到此代码。

将自定义设置导出到您的应用

一个常见的用例是通过自定义字段和属性设置器来扩展某个特定应用的 DocType。要将这些设置保存到应用中,请前往 自定义表单

您会看到一个 导出自定义设置 按钮

在这里,您可以选择模块,并决定是否希望在每次更新后同步这些特定的自定义设置。

自定义设置将导出到您应用模块文件夹中的一个新文件夹 custom 中。自定义设置将以 DocType 的名称保存

当您执行 bench update 或 bench migrate 时,这些自定义设置将同步到应用中。

警告:使用此功能时,每当自定义设置被同步,站点上的所有属性设置器和自定义权限都会被代码中指定的内容所替换。如果不需要这种行为,则不应使用此功能。

添加社交登录提供商

本指南介绍如何通过拉取请求(Pull Request)为 Frappe 添加社交登录提供商。

在 SocialLoginKey.get_social_login_provider 中添加你的提供商

providers["Frappe"] = {
 "provider_name": "Frappe",
 "enable_social_login": 1,
 "custom_base_url": 1,
 "icon":"/assets/frappe/images/favicon.png",
 "redirect_url": "/api/method/frappe.www.login.login_via_frappe",
 "api_endpoint": "/api/method/frappe.integrations.oauth2.openid_profile",
 "api_endpoint_args":None,
 "authorize_url": "/api/method/frappe.integrations.oauth2.authorize",
 "access_token_url": "/api/method/frappe.integrations.oauth2.get_token",
 "auth_url_data": json.dumps({
 "response_type": "code",
 "scope": "openid"
 })
}

在 Social Login Key DocType 的 social_login_provider 选择字段选项中,以完全相同的大小写形式添加提供商密钥。例如:Frappe

当用户添加社交登录提供商并启用后,提供商 API 服务器会将 Authorization Code 发送回同一服务器上指定的 redirect_url。你需要在 frappe.integrations.oauth2_logins 中添加一个允许访客访问的白名单方法。例如:login_via_office365

OAuth 2.0 + OpenID Connect 有多种实现方式。这里我们将讨论两种获取 OpenID 信息的方法。

通过 OpenID Profile 端点创建用户

示例:

@frappe.whitelist(allow_guest=True)
def login_via_frappe(code, state):
 login_via_oauth2("frappe", code, state, decoder=json.loads)

通过 id_token 创建用户

示例:

@frappe.whitelist(allow_guest=True)
def login_via_office365(code, state):
 login_via_oauth2_id_token("office_365", code, state, decoder=json.loads)

前提条件

  • 已连接应用

已连接应用 存储了您正在集成的平台的客户端 ID 和密钥、作用域以及端点。

快速开始

此示例使用 Google,但您可以使用任何支持 OAuth 2.0 的服务。

您的集成很可能需要一个设置文档类型(例如,“Google 设置”)。在这里,您创建一个指向 已连接应用 的链接字段。系统管理员稍后可以创建一个 已连接应用,输入客户端凭据并将其链接到您的集成。您将通过您的设置 DocType 知道哪个 已连接应用 属于您的集成。

google_settings = frappe.get_single('Google Settings')
connected_app_name = google_settings.get('connected_app')

connected_app = frappe.get_doc('Connected App', connected_app_name)

现在,您拥有了一个 已连接应用 的实例,其中包含凭据和端点 URI。如果用户尚未连接,他们可以打开 已连接应用 并点击右上角的“连接到…”按钮。或者,您可以获取授权 URI,并将其用于您自己的自定义按钮或重定向:

authorization_uri = connected_app.initiate_web_application_flow() # optional

一旦用户获得授权,您可以像这样获取一个经过身份验证的会话:

session = connected_app.get_oauth2_session()

如果此时用户未进行身份验证,他们将被重定向到身份验证页面。

现在,您可以从目标平台检索任何允许的资源:

session.get('https://www.googleapis.com/oauth2/v2/userinfo')

如果访问令牌已过期,会话将自动尝试刷新它。

单一类型单据

DocTypes 都有一个与之关联的数据库表。例如,DocType 客户 会有一个与之关联的表 tabCustomer。

单例类型的 DocTypes 没有关联的数据库表,并且只存在一个文档。这类似于 Java 中的单例模式。单例 DocTypes 非常适合用于保存设置(全局适用的设置),以及用于没有文档但 DocType 用于表单界面的向导/辅助类型表单。

单例 DocType 中的数据存储在 tabSingles(doctype、field、value)中。

示例

在 Frappe 中,单例类型包括 系统设置 和 自定义表单。

在桌面添加模块图标

Frappe 版本 12

要为你的应用创建图标,你需要编辑应用的 config/desktop.py 文件。在此文件中,你可以添加一个 get_data 方法,该方法返回一个包含模块图标参数的字典。

你还可以创建一个下拉列表,列出该模块可用的操作以及点击该模块后页面上的操作。要实现此功能,请创建一个文件 config/MODULE_NAME.py(将 MODULE_NAME 替换为你应用中的一个模块名称)。

示例:应用图书馆管理

config/desktop.py:

def get_data():
 return [
 {
 "module_name": "Library Management",
 "category": "Modules",
 "label": _("Library Management"),
 "color": "#589494",
 "icon": "octicon octicon-book",
 "type": "module",
 "description": "Library management"
 }
 ]

config/library_management.py:

def get_data():
 return [
 {
 "label": _("Library Management"),
 "icon": "octicon octicon-book",
 "items": [
 {
 "type": "doctype",
 "name": "Article",
 "label": _("Article"),
 "description": _("Manage Books"),
 "onboard": 1,
 },
 {
 "type": "doctype",
 "name": "Library Member",
 "label": _("Library Member"),
 "description": _("Manage Members"),
 # Not displayed on dropdown list action but on page after click on module
 "onboard": 0,
 }
 ]
 }
 ]

注意:模块视图根据权限显示。

将文档中的字段值提取到事务中

假设在“供应商”中有一个自定义字段“GSTIN”,需要在“采购订单”中获取该字段。

场景一:您希望保持此字段自动更新

在此场景中,当您保存采购订单时,该自定义字段将根据供应商中的值自动更新,并且每次保存采购订单时都会重新更新。由于此字段需要自动更新,因此它会覆盖用户输入。如果您希望允许用户输入,请参考场景二。

步骤:

  1. 为供应商文档创建一个自定义字段 GSTIN,将字段类型设置为数据。

  2. 为采购订单文档创建另一个自定义字段 GSTIN,但在此情况下,将字段类型设置为只读或勾选只读复选框。将从以下位置获取设置为 supplier.gstin。

  1. 转到用户菜单并点击“重新加载”。

  2. 现在,在新的采购订单中选择供应商后,GSTIN 将自动从所选供应商中获取。

场景二:如果未找到值,您希望允许用户输入

在此场景中,首次创建采购订单时会从供应商处获取该值。如果在供应商中未找到该值,您可以手动输入。仅当该字段为空时,才会在保存采购订单时获取该值。

步骤:

  1. 为供应商文档创建一个自定义字段 GSTIN,将字段类型设置为数据。

  1. 为采购订单文档创建另一个自定义字段 GSTIN,将字段类型设置为数据。将从以下位置获取设置为 supplier.vat_number,并勾选标题为为空时获取的复选框。

  1. 转到用户菜单并点击“重新加载”。

  2. 现在,在新的采购订单中选择供应商后,GSTIN 将自动从所选供应商中获取。如果在供应商中未找到 GSTIN,您可以手动输入。