单元测试

1.简介

Frappe 提供了一些基本工具,可以快速编写自动化测试。以下是一些基本规则:

  1. 测试可以放在仓库中的任何位置,但必须以 test_ 开头,并且应该是一个 .py 文件。
  2. 测试必须在以 test_ 开头的站点上运行。这是为了防止意外丢失数据。
  3. 新建 DocType 时会自动生成测试桩。
  4. Frappe 测试运行器会自动为通过 Link 类型字段(外键)标识的依赖 DocType 构建测试记录。
  5. 可以使用 bench run-tests 来执行测试。
  6. 对于非 DocType 测试,你可以编写简单的单元测试,并在文件名前加上 test_。

为 DocType 编写测试

2.1. 编写 DocType 测试:

  1. 测试用例放在名为 test_[doctype].py 的文件中。
  2. 你必须在测试文件中创建所有依赖项。
  3. 创建一个 Python 模块结构来生成测试数据/依赖项。

示例(针对 test_event.py):

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

import frappe
import frappe.defaults

from frappe.tests.utils import FrappeTestCase

def create_events():
    if frappe.flags.test_events_created:
        return

frappe.set_user("Administrator")
doc = frappe.get_doc({
    "doctype": "Event",
    "subject":"_Test Event 1",
    "starts_on": "2014-01-01",
    "event_type": "Public"
    }).insert()

doc = frappe.get_doc({
    "doctype": "Event",
    "subject":"_Test Event 2",
    "starts_on": "2014-01-01",
    "event_type": "Private"
    }).insert()

doc = frappe.get_doc({
    "doctype": "Event",
    "subject":"_Test Event 3",
    "starts_on": "2014-01-01",
    "event_type": "Public"
    "event_individuals": [{
        "person": "[email protected]"
    }]
    }).insert()

frappe.flags.test_events_created = True

class TestEvent(FrappeTestCase):
    def setUp(self):
        create_events()

    def tearDown(self):
        frappe.set_user("Administrator")

    def test_allowed_public(self):
        frappe.set_user("[email protected]")
        doc = frappe.get_doc("Event", frappe.db.get_value("Event",
        {"subject":"_Test Event 1"}))
        self.assertTrue(frappe.has_permission("Event", doc=doc))

    def test_not_allowed_private(self):
        frappe.set_user("[email protected]")
        doc = frappe.get_doc("Event", frappe.db.get_value("Event",
        {"subject":"_Test Event 2"}))
        self.assertFalse(frappe.has_permission("Event", doc=doc))

    def test_allowed_private_if_in_event_user(self):
        doc = frappe.get_doc("Event", frappe.db.get_value("Event",
        {"subject":"_Test Event 3"}))

        frappe.set_user("[email protected]")
        self.assertTrue(frappe.has_permission("Event", doc=doc))

    def test_event_list(self):
        frappe.set_user("[email protected]")
        res = frappe.get_list("Event", filters=[["Event", "subject", "like", "_Test Event%"]], fields=["name", "subject"])
        self.assertEqual(len(res), 2)
        subjects = [r.subject for r in res]
        self.assertTrue("_Test Event 1" in subjects)
        self.assertTrue("_Test Event 3" in subjects)
        self.assertFalse("_Test Event 2" in subjects)

2. 运行测试

这个函数会构建所有测试依赖项并运行你的测试。
你应该从 “frappe_bench” 文件夹运行测试。如果不加任何选项,所有测试都会被执行。

bench run-tests

如果你需要更多关于测试执行的信息,可以使用 bench 的详细日志级别。

bench –verbose run-tests

选项:

 --app <appname>
 --doctype 
<doctype>
 --test 
<specifictest>
 --module 
<module> (Run a particular module that has tests)
 --profile (Runs a Python profiler on the test)
 --junit-xml-output
<pathtoxml> (The command provides test results in the standard XUnit XML format)
</pathtoxml></module></specifictest></doctype></appname>

2.1. 应用示例:

所有应用都位于文件夹:”~/frappe-bench/apps”。
我们可以为每个应用运行测试。

  • frappe-bench/apps/erpnext/
  • frappe-bench/apps/erpnext_demo/
  • frappe-bench/apps/frappe/
 bench run-tests --app erpnext
 bench run-tests --app erpnext_demo
 bench run-tests --app frappe

2.2. DocType 示例:

 frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost"
 .
 ----------------------------------------------------------------------
 Ran 1 test in 0.008s

 OK

2.3. 测试示例:

运行 User 中的特定用例:

 frappe@erpnext:~/frappe-bench$ bench run-tests --doctype User --test test_get_value
 .
 ----------------------------------------------------------------------
 Ran 1 test in 0.005s

 OK

2.4. 模块示例:

如果我们想运行某个模块中的测试:

/home/frappe/frappe-bench/apps/erpnext/erpnext/support/doctype/issue/test_issue.py

我们应该这样使用模块名(相对于应用文件夹)

erpnext.support.doctype.issue.test_issue

示例:
 frappe@erpnext:~/frappe-bench$ bench run-tests --module "erpnext.stock.doctype.stock_entry.test_stock_entry"
 ...........................
 ----------------------------------------------------------------------
 Ran 27 tests in 30.549s

2.5. Profile 示例:

 frappe@erpnext:~/frappe-bench$ bench run-tests --doctype "Activity Cost" --profile
 .
 ----------------------------------------------------------------------
 Ran 1 test in 0.010s

 OK
 9133 function calls (8912 primitive calls) in 0.011 seconds

 Ordered by: cumulative time

 ncalls tottime percall cumtime percall filename:lineno(function)
 2 0.000 0.000 0.008 0.004 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:187(insert)
 1 0.000 0.000 0.003 0.003 /home/frappe/frappe-bench/apps/frappe/frappe/model/document.py:386(_validate)
 13 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/database.py:77(sql)
 255 0.000 0.000 0.002 0.000 /home/frappe/frappe-bench/apps/frappe/frappe/model/base_document.py:91(get)
 12 0.000 0.000 0.002 0.000

2.6 不创建测试数据或 before_tests 钩子来运行测试

  • 当你在开发功能时,使用 --skip-test-records 可以在不构建测试依赖项(即为关联对象构建测试数据)的情况下编写测试,这很有用。
  • 你也可以使用 --skip-before-tests 跳过测试初始化脚本。

示例

bench --site school.erpnext.local run-tests --doctype "Student Group" --skip-test-records --skip-before-tests

3.0 FrappeTestCase

FrappeTestCase 是 Frappe 框架特有的 TestCase 类,继承自 unittest.TestCase。在你的测试中继承这个类可以确保:

  1. frappe.local.flags 和其他最常用的本地代理在测试用例运行后会被重置。
  2. 数据库——在测试用例开始前会启动一个新的数据库事务,并在测试完成后回滚。

用法

# app/doctype/dt/test_dt.py  

from frappe.tests.utils import FrappeTestCase  

class TestDt(FrappeTestCase):  
 @classmethod  
 def setUpClass(cls):  
 super().setUpClass() # important to call super() methods when extending TestCase.   
 ...

编写 XUnit XML 测试

如何运行:
bench run-tests --junit-xml-output=/reports/junit_test.xml
测试报告示例:
 <testsuite tests="3">
 <testcase classname="foo1" name="ASuccessfulTest">
 <testcase classname="foo2" name="AnotherSuccessfulTest">
 <testcase classname="foo3" name="AFailingTest">
 <failure type="NotEnoughFoo"> details about failure </failure>
 </testcase>
 </testcase></testcase></testsuite>

它是为 CI Jenkins 设计的,但也适用于任何其他能理解 XUnit 格式 XML 测试结果表示的工具。

Jenkins 配置支持:

  1. 你应该安装 xUnit 插件——https://wiki.jenkins-ci.org/display/JENKINS/xUnit+Plugin
  2. 安装完成后,打开 Jenkins 任务配置,在 “Post-build Actions” 下点击名为 “Publish JUnit test result report” 的复选框,然后输入 XML 报告的路径:

(示例:reports/.xml*)

自动化测试

Frappe 为你提供了一个测试框架,用于编写和执行测试,这些测试可以直接在像 Travis 这样的持续集成工具上运行

你可以编写服务器端单元测试或 UI 测试

从从库/辅助MySQL系统设置读取操作

正常情况下,系统会在同一个数据库上进行读写操作。随着数据量增加或并发活动增多,你可能会开始遇到操作延迟的问题。

避免这种延迟的一个直接解决方案是,将读写操作分开,即从副本/从数据库读取所有数据,将所有信息写入主/主数据库。

现在,在 frappe 中,你可以在主数据库和副本之间拆分读写操作。

设置只读环境的步骤

  1. 前提条件:
    你应该已经搭建好 MariaDB 主从或集群环境。

  2. 只读副本的配置:

在你的 site_config.json 中,添加以下键以启用从副本/从系统读取。

...
"read_from_replica" : 1/0 # to enable disable read from replica
"different_credentials_for_replica" : 1/0 #if database creadetials are different on replica then set 1 else 0
"replica_host" : "IP address for replica" ,
"replica_db_port" : "Replica DB port",
"replica_db_name" : "Replica DB name",
"replica_db_password" : "Replica DB password",
...

注意:如果你已启用 MariaDB 主从环境,那么两个系统上的数据库名称和数据库密码是相同的。

  1. 在从/从系统上授予主主机访问权限。

操作文档类型

文档类型(DocType)是一种特定类型的文档,例如:客户、员工或物料。

文档名称(DocumentName)是文档的唯一标识符,例如:CUST-00001、EMP-00001 或 ITEM-00001。

以下示例中缺少身份验证。更多信息请参阅[基本身份验证]和[OAuth2]。

GET /api/resource/{DocType}

获取该文档类型下的文档列表。

路径参数:

  • DocType(字符串)

你想要获取的文档类型。例如:’Customer’。

查询参数:

  • fields []

默认情况下,只返回 ‘name’ 字段。要添加更多字段,可以传入 fields 参数。例如:fields=[“name”,”country”]

  • filters [[(string)]]

你可以通过在 filters 参数中传入 SQL 条件来筛选列表。每个条件的格式为 [{doctype}, {field}, {operator}, {value}]。例如:filters=[[“Customer”, “country”, “=”, “India”]]

  • limit_page_length(整数)

所有列表都会分页显示。通过此参数可以更改页面大小(一次返回多少条记录)。默认值:20。

  • limit_start(整数)

要请求后续页面,请传入 limit_page_length 的倍数作为 limit_start。例如,要请求第二页,请将 limit_start 设为 20。

示例:

获取所有电话号码为 4915227058038 的客户的最多 20 个名称(ID)。

curl -X GET https://{your frappe instance}/api/resource/Customer?fields=["name"]\
 &filters=[["Customer","phone","=","4915227058038"]]

返回值:

 {
 "data": [
 {
 "name": "CUST-00001"
 },
 ]
 }

POST /api/resource/{DocType}

创建该文档类型的新文档。

路径参数:

  • DocType(字符串)

你想要创建的文档类型。例如:’Customer’。

Content-Type:application/json

请求体:{"fieldname": value}

示例:

创建一个名为 Mustermann 的新线索。

curl -X POST https://{your frappe instance}/api/resource/Lead \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -d '{"lead_name":"Mustermann"}'

GET /api/resource/{DocType}/{DocumentName}

按名称(ID)获取特定文档。

路径参数:

  • DocType(字符串)

你想要获取的文档类型。例如:’Customer’。

  • DocumentName(字符串)

你想要获取的文档名称(ID)。例如:’EMP-00001’。

示例:

获取名称为(ID)CUST-00001 的客户。

curl -X GET https://{your frappe instance}/api/resource/Customer/CUST-00001

PUT /api/resource/{DocType}/{DocumentName}

更新特定文档。这类似于 PATCH HTTP 请求,你不需要发送整个文档,只需发送要更改的部分。

路径参数:

  • DocType(字符串)

你想要更新的文档类型。例如:’Customer’。

  • DocumentName(字符串)

你想要更新的文档名称(ID)。例如:’EMP-00001’。

Content-Type:application/json

请求体:{"fieldname": value}

示例:

更新下次联系日期。

curl -X PUT https://{your frappe instance}/api/resource/Lead/LEAD-00001 \
 -H 'Accept: application/json' \
 -H 'Content-Type: application/json' \
 -d '{"contact_date":"2018-10-08"}'

返回值:

{
 "data": {
 "doctype": "Lead",
 "name": "LEAD-00001",
 "contact_date": "2018-10-08",
 "...": "..."
 }
}

DELETE /api/resource/{DocType}/{DocumentName}

路径参数:

  • DocType(字符串)

你想要删除的文档类型。例如:’Customer’。

  • DocumentName(字符串)

你想要删除的文档名称(ID)。例如:’EMP-00001’。

延伸阅读

HTTP 请求头:

  • Content-Type
  • Accept
  • Authorization

作者:Raffael Meyer([email protected])

列出文档

要列出文档,请向 /api/resource/{doctype} 发送 GET 请求。

GET /api/resource/Person

所有列表默认每页返回 20 条记录。要更改每页大小,可以传递查询参数 limit_page_length。要请求后续页面,请传递 limit_start。

响应以 JSON 对象形式返回,列表是一个数组,键为 data。

响应:

{
 "data": [
 {
 "name": "000000012"
 },
 {
 "name": "000000008"
 }
 ]
}

默认情况下,列表中只包含 name 字段。要添加更多字段,可以在 GET 请求中传递 fields 参数。fields 必须是一个包含字段名的 JSON 数组。

GET /api/resource/Person/?fields=[“name”,”first_name”]

响应:

{
 "data": [
 {
 "first_name": "Jane",
 "name": "000000012"
 },
 {
 "first_name": "John",
 "name": "000000008"
 }
 ]
}

你可以通过传递查询参数 filters 来使用 SQL 条件筛选列表。filters 必须是一个包含一个或多个过滤条件的 JSON 数组。每个条件的格式为 [{doctype}, {field}, {operator}, {operand}]。

例如,获取所有名字为 “Jane” 的人员的 name(id):

GET /api/resource/Person?filters=[[“Person”,”first_name”,”=”,”Jane”]]

响应:

 { "data": [ { "name": "000000012" } ] }

作者:Rushabh Mehta ([email protected])、Raffael Meyer ([email protected])

OAuth 2

使用请求头 Authorization: Bearer <access_token></access_token> 来执行需要身份验证的请求。你可以通过组合以下两个请求来获取一个持有者令牌(bearer token)。

这里有一篇非常棒的 OAuth 入门介绍:OAuth 2.0 和 OpenID Connect(通俗易懂版)

GET /api/method/frappe.integrations.oauth2.authorize

获取用户的授权码,以便代表用户访问 ERPNext。

参数(在查询字符串中):

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • state(字符串)

你的客户端应用程序用来在请求和回调之间维护状态的任意值。授权服务器在将用户代理重定向回客户端时会包含这个值。该参数应用于防止跨站请求伪造。

  • response_type(字符串)

“code”

  • scope(字符串)

应授予你的应用程序的访问范围。

  • redirect_uri(字符串)

应用程序授权后,用户将被重定向到的回调 URI。然后可以从参数中提取授权码。

  • code_challenge_method(字符串)

(可选)可以是 s256 或 plain 中的一个。

  • code_challenge(字符串)

(可选)当 code_challenge_method=s256 时可以是 base64encode(sha256(random_string)),或者当 code_challenge_method=plain 时可以是 random_string。参考 https://tools.ietf.org/html/rfc7636#appendix-A

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.authorize \
 --data-urlencode 'client_id=511cb2ac2d' \
 --data-urlencode 'state=444' \
 # base64encode(sha256('420')) => 21XaP8MJjpxCMRxgEzBP82sZ73PRLqkyBUta1R309J0
 # --data-urlencode 'code_verifier=21XaP8MJjpxCMRxgEzBP82sZ73PRLqkyBUta1R309J0' \
 --data-urlencode 'response_type=code'
 --data-urlencode 'scope=openid%20all' \
 --data-urlencode 'redirect_uri=https://app.getpostman.com/oauth2/callback'

返回:

  • HTTP 状态码:200
  • text/html

这将打开授权页面,然后会将你重定向到 redirect_uri。

如果用户点击”允许”,重定向 URI 将被调用,并在查询参数中附带授权码:

https://{redirect uri}?code=plkj2mqDLwaLJAgDBAkyR1W8Co08Ud&state=444

如果用户点击”拒绝”,你将收到一个错误:

https://{redirect uri}?error=access_denied

使用 ID Token 的授权码授予的令牌交换

POST /api/method/frappe.integrations.oauth2.get_token
Header Content-Type: application/x-www-form-urlencoded

注意:此端点也可用于获取刷新的访问令牌。只需在请求体中发送 refresh_token 即可。

用(上面获取的)授权码换取访问令牌。

参数(在请求体中):

  • grant_type(字符串)

“authorization_code”

  • code(字符串)

在重定向 URI 中收到的授权码。

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • redirect_uri(字符串)

客户端应用注册的重定向 URI

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token \
 -H 'Content-Type: application/x-www-form-urlencoded' \
 -H 'Accept: application/json' \
 -d 'grant_type=authorization_code&code=wa1YuQMff2ZXEAu2ZBHLpJRQXcGZdr
 &redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&client_id=af615c2d3a'

出于测试目的,你也可以像这样在 URL 中传递参数(并在浏览器中打开):

https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token?grant_type=authorization_code&code=A1KBRoYAN1uxrLAcdGLmvPKsRQLvzj&client_id=511cb2ac2d&redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback

返回:

 {
 "access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1",
 "scope": "openid all",
 "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o"
 }

使用 ID Token 的授权码授予的令牌交换(PKCE)

POST /api/method/frappe.integrations.oauth2.get_token
Header Content-Type: application/x-www-form-urlencoded

用(上面获取的)授权码换取访问令牌。

参数(在请求体中):

  • grant_type(字符串)

“authorization_code”

  • code(字符串)

在重定向 URI 中收到的授权码。

  • client_id(字符串)

你的 OAuth2 应用程序的 ID

  • redirect_uri(字符串)

客户端应用注册的重定向 URI

  • code_verifier(字符串)

在 Authorization Request 期间与 code_challenge_method 和 code_challenge 一起使用的 random_string。

Content-Type: application/x-www-form-urlencoded

示例:

curl -X POST https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token \
 -H 'Content-Type: application/x-www-form-urlencoded' \
 -H 'Accept: application/json' \
 -d 'grant_type=authorization_code&code=wa1YuQMff2ZXEAu2ZBHLpJRQXcGZdr
 &redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&client_id=af615c2d3a&code_verifier=420'

出于测试目的,你也可以像这样在 URL 中传递参数(并在浏览器中打开):

https://{your frappe instance}/api/method/frappe.integrations.oauth2.get_token?grant_type=authorization_code&code=A1KBRoYAN1uxrLAcdGLmvPKsRQLvzj&client_id=511cb2ac2d&redirect_uri=https%3A%2F%2Fapp.getpostman.com%2Foauth2%2Fcallback&code_verifier=420

返回:

{
 "access_token": "pNO2DpTMHTcFHYUXwzs74k6idQBmnI",
 "token_type": "Bearer",
 "expires_in": 3600,
 "refresh_token": "cp74cxbbDgaxFuUZ8Usc7egYlhKbH1",
 "scope": "openid all",
 "id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o"
}

撤销令牌端点

POST /api/method/frappe.integrations.oauth2.revoke_token
Header: Content-Type: application/x-www-form-urlencoded

撤销令牌端点。

参数:

  • token

要撤销的访问令牌。

返回:

始终返回空响应,HTTP 状态码为 200。

 {}

Open ID Connect id_token

ID Token 是一个 JWT。

  • aud 声明包含已注册的 client_id。
  • iss 声明包含 frappe 服务器 URL。
  • sub 声明包含 Frappe 用户的 userid。
  • roles 声明包含用户角色。
  • exp 声明包含过期时间。
  • iat 声明包含签发时间。

示例:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkouIERvZSIsImVtYWlsIjoiakBkb2UuY29tIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDIwMjIsImF1ZCI6ImNsaWVudF9pZCJ9.ZEdnrHjLbArahVTN19b4zoRFoBO5a2BakRkR82O1VU8

使用 PyJWT 验证并提取它。

import jwt

id_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkouIERvZSIsImVtYWlsIjoiakBkb2UuY29tIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDIwMjIsImF1ZCI6ImNsaWVudF9pZCIsInJvbGVzIjpbIlN5c3RlbSBNYW5hZ2VyIiwiU2FsZXMgTWFuYWdlciJdLCJub25jZSI6Ijc4OTEyMyIsImlzcyI6Imh0dHBzOi8vZXJwLmV4YW1wbGUuY29tIn0.F8Wbh5dtD1loZPltJLj_sqF9DZNeBvEbo-ITtf3UPqk"

client_id = 'client_id'
client_secret = 'client_secret'

payload = jwt.decode(
 id_token,
 audience=client_id,
 key=client_secret,
 algorithm="HS256",
 options={"verify_exp": False}, # Enabled by default to verify expiration time
)

print(payload)
# Output

{'sub': '1234567890', 'name': 'J. Doe', 'email': '[email protected]', 'iat': 1516239022, 'exp': 1516242022, 'aud': 'client_id', 'roles': ['System Manager', 'Sales Manager'], 'nonce': '789123', 'iss': 'https://erp.example.com'}

OpenID 用户信息端点

请求

GET /api/method/frappe.integrations.oauth2.openid_profile
Header: Authorization: Bearer valid_access_token

响应

{
 "sub": "1234567890",
 "name": "J. Doe",
 "given_name": "J",
 "family_name": "Doe",
 "iss": "https://erp.example.com",
 "picture": "https://erp.example.com/files/jdoe.jpg",
 "email": "[email protected]",
 "iat": 1516239022,
 "exp": 1516242022,
 "aud": "client_id",
 "roles": ["System Manager", "Sales Manager"]
}

内省令牌端点

POST /api/method/frappe.integrations.oauth2.introspect_token
Header: Content-Type: application/x-www-form-urlencoded

撤销令牌端点。

参数:

  • token_type_hint

access_token 或 refresh_token,如果未提供则默认为 access_token

  • token

要内省的访问令牌或刷新令牌。取决于 token_type_hint

返回:

{
 "client_id": "511cb2ac2d",
 "trusted_client": 1,
 "active": true,
 "exp": 1619523326,
 "scope": "openid all",
 "sub": "1234567890",
 "name": "J. Doe",
 "given_name": "J",
 "family_name": "Doe",
 "iss": "https://erp.example.com",
 "picture": "https://erp.example.com/files/jdoe.jpg",
 "email": "[email protected]",
 "iat": 1516239022,
 "exp": 1516242022,
 "aud": "511cb2ac2d",
 "roles": ["System Manager", "Sales Manager"]
}

或者

{
 "active": false,
 "_server_messages": "..."
}

进一步阅读

请查看 Guides / Integration / How To Set Up Oauth 了解如何创建新的 OAuth 2 客户端。

  • Content-Type 请求头,
  • Authorization 请求头,
  • OAuth 2 规范,
  • 持有者令牌(Bearer token)。

作者:

  • Raffael Meyer ([email protected])
  • Revant Nandgaonkar ([email protected])

项目、客户和供应商的公司限制

公司限制功能可以将物料、客户或供应商限制在指定的公司范围内。被限制的记录只能在你列出的公司中使用。ERPNext 会在其他所有公司中阻止该记录的使用。

在多公司站点中,如果某个主数据只属于一家公司,就可以使用此功能。例如,某个产品只有一家公司销售,或者某个供应商只有一家公司向其采购。

字段说明

物料、客户和供应商的公司限制部分包含两个字段。

字段 类型 说明
限制到公司 复选框 为此记录开启限制。
允许的公司 表格多选 可以使用此记录的公司。

勾选限制到公司后,允许的公司为必填项。如果取消勾选,ERPNext 会清空该列表。

谁可以更改限制

该部分位于权限级别 1。只有以下角色可以读取和写入这两个字段。

单据 角色
物料 物料管理员
客户 销售主数据管理员
供应商 采购主数据管理员

其他用户看不到该部分。如果其他用户保存了记录,ERPNext 会保留原有的限制值。

限制一条记录

  1. 打开物料、客户或供应商。
  2. 找到公司限制部分。
  3. 勾选限制到公司。
  4. 在允许的公司中添加一个或多个公司。
  5. 保存记录。

允许的公司字段只显示你可以访问的公司。如果你对公司有用户权限,则无法添加或移除该权限范围之外的公司。

限制的作用

交易

当你保存带有公司字段的单据时,ERPNext 会检查限制。检查会读取单据及其子表中所有指向物料、客户或供应商的链接。如果被链接的记录受到限制,且单据的公司不在允许列表中,保存会失败并显示以下消息:

由于公司限制,物料 ITEM-0001 无法与公司 Wind Power LLC 一起使用

该检查覆盖所有交易,例如销售订单、采购发票、库存条目和物料申请。它也覆盖带有公司字段的主数据,例如物料清单和定价规则。

往来方选择

当你在交易中选择受限制的客户或供应商时,ERPNext 在获取往来方详细信息时会抛出相同的错误。你无需保存单据就能看到该错误。

销售和采购交易中的物料、客户和供应商搜索会发送单据的公司信息。这些搜索会隐藏该公司无法使用的记录。不发送公司信息的搜索会显示所有记录。保存时的检查仍会阻止错误的记录。

列表视图和报表

只有当用户对公司有用户权限时,列表视图过滤才会生效。此类用户只能看到允许列表中包含其被许可公司的受限制物料、客户或供应商。该用户也无法打开被许可公司之外的受限制记录。

没有公司用户权限的用户可以看到列表中的所有记录。交易检查仍然对该用户生效。

限制条件

  • 检查在保存时运行。已提交的单据在你更改限制后仍然有效。你仍然可以取消该单据。
  • 修订后的单据会再次运行检查。如果限制现在阻止了该公司,修订会失败。
  • 限制不会禁用记录。记录在其允许的公司中保持活跃。

消息

消息 原因 操作
勾选限制到公司时,允许的公司为必填项 复选框已开启且列表为空。 添加至少一个公司,或取消勾选。
由于公司限制,<doctype> <name></name></doctype> 无法与公司 <company></company> 一起使用 该文档使用了公司中不允许使用的受限记录。 从文档中移除该记录,或将该公司添加到允许的公司中。
您无权在允许的公司中添加或移除公司 <company></company> 您在公司上的用户权限不涵盖该公司。 请让管理员更改列表。

基于令牌的身份验证

自 v11.0.3 版本起可用

HTTP Authorization 请求头包含用于向服务器验证用户身份的凭据。它由授权类型(token 或 Basic)和相应的令牌组成。

Authorization: <type> <token>
</token></type>

该令牌由 api-key 和 api-secret 通过冒号连接而成。

生成 API 密钥和 API 机密

  1. 前往用户列表并打开一个用户。
  2. 点击“设置”选项卡。(如果看不到选项卡,请跳过此步骤)
  3. 展开 API 访问部分,然后点击“生成密钥”。
  4. 您将看到一个包含 API 机密的弹窗。请复制此值并妥善保管(例如密码管理器)。
  5. 您还会在此部分看到另一个字段“API 密钥”。

现在,使用这两个密钥即可对 API 请求进行身份验证。使用这些密钥发出的每个请求都将记录到您在步骤 1 中选择的用户名下。这也意味着系统将针对该用户进行角色权限检查。您也可以专门为 API 调用创建一个新用户。

令牌

HTTP 请求头:

Authorization: token <api_key>:<api_secret>
</api_secret></api_key>

Python 示例:

import requests

url = "http://frappe.local:8000/api/method/frappe.auth.get_logged_user"
headers = {
    'Authorization': "token 
<api_key>:<api_secret>"
}
response = requests.request("GET", url, headers=headers)
</api_secret></api_key>

基本认证

如果使用“基本”认证方案,凭据由 api_key 和 api_secret 组合而成,构造方式如下:

  1. 将两个值用冒号连接:<api_key>:<api_secret></api_secret></api_key>
  2. 将生成的字符串进行 base64 编码:base64encode(<api_key>:<api_secret>)</api_secret></api_key>

HTTP 请求头:

Authorization: Basic base64encode("<api_key>:<api_secret>")
</api_secret></api_key>

Python 示例:

import requests
import base64

url = "http://frappe.local:8000**/api/method/frappe.auth.get_logged_user**"
headers = {
    'Authorization': "Basic %s" % base64.b64encode(
<api_key>:<api_secret>)
}
response = requests.request("GET", url, headers=headers)
</api_secret></api_key>

访问令牌

如果使用 OAuth 2 访问令牌来验证请求,该令牌是 Frappe Server 在设置 OAuth 2 并生成令牌后提供的不透明 access_token 字符串。请参阅 Guides / Integration / How To Use OAuth 2

HTTP 请求头:

Authorization: Bearer access_token

Python 示例:

import requests
import base64

url = "http://frappe.local:8000**/api/method/frappe.auth.get_logged_user**"
headers = {
    "Authorization": "Bearer %s" % access_token
}
response = requests.request("GET", url, headers=headers)

更多资源

  • 授权请求头

简单认证

POST /api/method/login

内容类型:application/x-www-form-urlencoded

参数(在请求体中):

  • usr(字符串)

用户名

  • pwd(字符串)

密码

示例:

curl -X POST https://{your frappe instance}/api/method/login \
 -H 'Content-Type: application/json' \
 -H 'Accept: application/json' \
 -d '{"usr":"Administrator","pwd":"admin"}'

返回结果:

  • HTTP 状态码:200
  • application/json:
 {
 "home_page": "/desk",
 "full_name": "Administrator",
 "message": "Logged in"
 }
  • Cookie:sid(发送此 Cookie 以验证后续请求)。有效期三天。
 sid=05d8d46aaebff1c87a90f570a3ff1c0f570a3ff1c87a90f56bacd4;
 path=/;
 domain=.{your frappe instance};
 Expires=Sat, 29 Sep 2018 00:59:54 GMT;

错误信息:

  • HTTP 状态码:401
  • text/html:密码或用户名错误。

GET /api/method/logout

示例:

 curl -X GET https://{your frappe instance}/api/method/logout

返回结果:

  • HTTP 状态码:200
  • application/json:{}

GET /api/method/frappe.auth.get_logged_user

获取当前已认证用户的 ID。

示例:

 curl -X GET https://{your frappe instance}/api/method/frappe.auth.get_logged_user

返回结果:

  • HTTP 状态码:200
  • application/json:
 {
 "message": "Administrator"
 }

作者:Raffael Meyer([email protected])