forked from erp-dev/erp
fix: admin of printing module
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
6
.idea/copilot.data.migration.agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
6
.idea/copilot.data.migration.ask.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AskMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
6
.idea/copilot.data.migration.ask2agent.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Ask2AgentMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
6
.idea/copilot.data.migration.edit.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="EditMigrationStateService">
|
||||||
|
<option name="migrationStatus" value="COMPLETED" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
29
.idea/flower.iml
generated
Normal file
29
.idea/flower.iml
generated
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="django" name="Django">
|
||||||
|
<configuration>
|
||||||
|
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||||
|
<option name="settingsModule" value="flower/settings.py" />
|
||||||
|
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
|
||||||
|
<option name="environment" value="<map/>" />
|
||||||
|
<option name="doNotUseTestRunner" value="false" />
|
||||||
|
<option name="trackFilePattern" value="migrations" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.14 (flower)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
<component name="TemplatesService">
|
||||||
|
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (flower)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/flower.iml" filepath="$PROJECT_DIR$/.idea/flower.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -175,12 +175,6 @@ class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer):
|
|||||||
raise serializers.ValidationError("数量必须大于0")
|
raise serializers.ValidationError("数量必须大于0")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_pieces(self, value):
|
|
||||||
"""验证件数(仅在提供值时验证)"""
|
|
||||||
if value is not None and value <= 0:
|
|
||||||
raise serializers.ValidationError("件数必须大于0")
|
|
||||||
return value
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
"""创建任务,使用 service 层"""
|
"""创建任务,使用 service 层"""
|
||||||
user = self.context['request'].user
|
user = self.context['request'].user
|
||||||
@@ -197,7 +191,6 @@ class PrintingJobCreateUpdateSerializer(serializers.ModelSerializer):
|
|||||||
return updated_instance
|
return updated_instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PlateOrderListSerializer(serializers.ModelSerializer):
|
class PlateOrderListSerializer(serializers.ModelSerializer):
|
||||||
"""开版订单列表序列化器"""
|
"""开版订单列表序列化器"""
|
||||||
customer_name = serializers.CharField(source="customer.name", read_only=True)
|
customer_name = serializers.CharField(source="customer.name", read_only=True)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class PlateOrderAdmin(admin.ModelAdmin):
|
|||||||
messages.append(f"{obj.id}: 没有关联的流程实例,无法推进")
|
messages.append(f"{obj.id}: 没有关联的流程实例,无法推进")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
success, message, _ = services.advance_to_next_state(obj.business_object, request.user)
|
success, message, state_log = services.advance_to_next_state(obj.business_object, request.user)
|
||||||
if success:
|
if success:
|
||||||
success_count += 1
|
success_count += 1
|
||||||
messages.append(f"{obj.id}: {message}")
|
messages.append(f"{obj.id}: {message}")
|
||||||
@@ -383,7 +383,7 @@ class PrintingJobAdmin(admin.ModelAdmin):
|
|||||||
messages.append(f"PrintingJob #{obj.id}: 没有关联的业务对象,无法推进")
|
messages.append(f"PrintingJob #{obj.id}: 没有关联的业务对象,无法推进")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
success, message = services.advance_to_next_state(obj.business_object, request.user)
|
success, message, state_log = services.advance_to_next_state(obj.business_object, request.user)
|
||||||
if success:
|
if success:
|
||||||
success_count += 1
|
success_count += 1
|
||||||
messages.append(f"PrintingJob #{obj.id}: {message}")
|
messages.append(f"PrintingJob #{obj.id}: {message}")
|
||||||
|
|||||||
275
stateflow/API.md
275
stateflow/API.md
@@ -14,10 +14,285 @@ Stateflow API 提供了状态节点和流程的 CRUD 操作接口。
|
|||||||
|
|
||||||
所有 API 都需要 JWT 认证。在请求头中添加:
|
所有 API 都需要 JWT 认证。在请求头中添加:
|
||||||
|
|
||||||
|
# Stateflow API 文档 (v2)
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Stateflow API 提供了状态节点、流程模板以及流程实例(业务对象)的完整生命周期管理,包括创建、查询、更新、删除以及核心的状态流转操作。
|
||||||
|
|
||||||
|
## 基础 URL
|
||||||
|
|
||||||
|
所有 Stateflow 相关 API 都在以下基础路径下:
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/v1/stateflow/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 认证
|
||||||
|
|
||||||
|
所有 API 都需要 JWT 认证。在请求头中添加:
|
||||||
|
|
||||||
```
|
```
|
||||||
Authorization: Bearer <access_token>
|
Authorization: Bearer <access_token>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 分页
|
||||||
|
|
||||||
|
列表接口默认使用 `LimitOffset` 分页:
|
||||||
|
|
||||||
|
- `limit`: 返回结果数量。
|
||||||
|
- `offset`: 偏移量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. State API (状态节点)
|
||||||
|
|
||||||
|
**基础路径**: `/api/v1/stateflow/states/`
|
||||||
|
|
||||||
|
管理流程中的基础单元:状态。
|
||||||
|
|
||||||
|
### 1.1. 列表查询
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/states/`
|
||||||
|
- **描述**: 获取状态节点列表。
|
||||||
|
- **查询参数**:
|
||||||
|
- `name`: 按名称精确查询。
|
||||||
|
- `search`: 按名称和描述进行模糊搜索。
|
||||||
|
- `ordering`: 排序字段 (`id`, `name`, `created_at`, `updated_at`)。
|
||||||
|
|
||||||
|
### 1.2. 详情查询
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/states/{id}/`
|
||||||
|
- **描述**: 获取单个状态节点的详细信息,包含其关联的参数模板。
|
||||||
|
|
||||||
|
### 1.3. 创建状态
|
||||||
|
|
||||||
|
- **POST** `/api/v1/stateflow/states/`
|
||||||
|
- **描述**: 创建一个新的状态节点。
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "待质检",
|
||||||
|
"description": "等待品质检验",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"key": "inspector",
|
||||||
|
"value": "",
|
||||||
|
"is_required": true,
|
||||||
|
"description": "质检员"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4. 更新状态
|
||||||
|
|
||||||
|
- **PUT/PATCH** `/api/v1/stateflow/states/{id}/`
|
||||||
|
- **描述**: 完全或部分更新一个状态节点。`PUT` 请求会替换所有字段,`PATCH` 只更新提供的字段。`parameters` 数组在 `PUT` 时会完全替换。
|
||||||
|
|
||||||
|
### 1.5. 删除状态
|
||||||
|
|
||||||
|
- **DELETE** `/api/v1/stateflow/states/{id}/`
|
||||||
|
- **描述**: 删除一个状态节点。
|
||||||
|
|
||||||
|
### 1.6. 获取状态参数
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/states/{id}/parameters/`
|
||||||
|
- **描述**: 获取指定状态的参数列表。
|
||||||
|
- **查询参数**:
|
||||||
|
- `required_only`: `true` 或 `false`,是否只返回必填参数。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Process API (流程模板)
|
||||||
|
|
||||||
|
**基础路径**: `/api/v1/stateflow/processes/`
|
||||||
|
|
||||||
|
管理可复用的流程模板。
|
||||||
|
|
||||||
|
### 2.1. 列表查询
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/processes/`
|
||||||
|
- **描述**: 获取流程模板列表。
|
||||||
|
- **查询参数**:
|
||||||
|
- `name`: 按名称精确查询。
|
||||||
|
- `search`: 按名称和描述进行模糊搜索。
|
||||||
|
- `ordering`: 排序字段 (`id`, `name`, `node_count`, `created_at`, `updated_at`)。
|
||||||
|
|
||||||
|
### 2.2. 详情查询
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/processes/{id}/`
|
||||||
|
- **描述**: 获取单个流程模板的详细信息,包含其所有节点(`nodes`)。
|
||||||
|
|
||||||
|
### 2.3. 创建流程
|
||||||
|
|
||||||
|
- **POST** `/api/v1/stateflow/processes/`
|
||||||
|
- **描述**: 创建一个新的流程模板,并定义其节点顺序。
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "生产流程",
|
||||||
|
"description": "从下单到发货的标准流程",
|
||||||
|
"nodes": [
|
||||||
|
{ "state_id": 1, "order": 0 },
|
||||||
|
{ "state_id": 2, "order": 1 },
|
||||||
|
{ "state_id": 5, "order": 2 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4. 更新流程
|
||||||
|
|
||||||
|
- **PUT/PATCH** `/api/v1/stateflow/processes/{id}/`
|
||||||
|
- **描述**: 更新流程模板。`nodes` 数组在 `PUT` 时会完全替换。
|
||||||
|
|
||||||
|
### 2.5. 删除流程
|
||||||
|
|
||||||
|
- **DELETE** `/api/v1/stateflow/processes/{id}/`
|
||||||
|
- **描述**: 删除一个流程模板。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Business Object API (流程实例)
|
||||||
|
|
||||||
|
**基础路径**: `/api/v1/stateflow/business-objects/`
|
||||||
|
|
||||||
|
管理流程的具体实例,这是状态流转的核心。
|
||||||
|
|
||||||
|
### 3.1. 列表查询
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/`
|
||||||
|
- **描述**: 获取流程实例列表。
|
||||||
|
- **查询参数**:
|
||||||
|
- `name`: 按名称模糊查询。
|
||||||
|
- `process`: 按流程模板 ID 过滤。
|
||||||
|
- `process_name`: 按流程模板名称模糊查询。
|
||||||
|
- `overall_status`: 按整体状态过滤 (`not_started`, `in_progress`, `completed`)。
|
||||||
|
- `content_type_str`: 按关联的业务模型过滤,格式: `app_label.model` (例如 `printing.plateorder`)。
|
||||||
|
- `has_content_object`: `true` 或 `false`,过滤是否有关联的业务模型。
|
||||||
|
- `search`: 按名称和描述进行模糊搜索。
|
||||||
|
- `ordering`: 排序字段 (`id`, `name`, `created_at`, `updated_at`)。
|
||||||
|
|
||||||
|
### 3.2. 详情查询
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/{id}/`
|
||||||
|
- **描述**: 获取单个流程实例的详细信息,包含当前状态、进度、时间线等。
|
||||||
|
|
||||||
|
### 3.3. 创建流程实例
|
||||||
|
|
||||||
|
- **POST** `/api/v1/stateflow/business-objects/`
|
||||||
|
- **描述**: 创建一个新的流程实例。
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "订单 #123 的生产流程",
|
||||||
|
"process": 1,
|
||||||
|
"description": "客户A的加急订单"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4. 更新流程实例
|
||||||
|
|
||||||
|
- **PUT/PATCH** `/api/v1/stateflow/business-objects/{id}/`
|
||||||
|
- **描述**: 更新流程实例的基本信息(如名称、描述)。
|
||||||
|
|
||||||
|
### 3.5. 删除流程实例
|
||||||
|
|
||||||
|
- **DELETE** `/api/v1/stateflow/business-objects/{id}/`
|
||||||
|
- **描述**: 删除一个流程实例及其所有相关日志。
|
||||||
|
|
||||||
|
### 3.6. 核心流转操作 (Custom Actions)
|
||||||
|
|
||||||
|
#### 3.6.1. 推进到下一状态
|
||||||
|
|
||||||
|
- **POST** `/api/v1/stateflow/business-objects/{id}/advance/`
|
||||||
|
- **描述**: 将流程实例从当前状态推进到下一个状态。
|
||||||
|
- **请求体** (可选):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"temperature": "25.5",
|
||||||
|
"humidity": "60%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **成功响应**: `200 OK`,包含成功信息和更新后的业务对象。
|
||||||
|
- **失败响应**: `400 Bad Request`,如果缺少必填参数或流程已完成。
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "流程已完成,无法继续推进"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.6.2. 回退一步
|
||||||
|
|
||||||
|
- **POST** `/api/v1/stateflow/business-objects/{id}/step_back/`
|
||||||
|
- **描述**: 撤销最后一次完成的状态,使流程回退一步。
|
||||||
|
- **成功响应**: `200 OK`。
|
||||||
|
- **失败响应**: `400 Bad Request`,如果从未开始过。
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "没有任何状态流转记录,无法回退"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.6.3. 重置进度
|
||||||
|
|
||||||
|
- **POST** `/api/v1/stateflow/business-objects/{id}/reset/`
|
||||||
|
- **描述**: 撤销所有已完成的状态,将流程实例重置到未开始状态。
|
||||||
|
|
||||||
|
### 3.7. 状态查询接口 (Custom Actions)
|
||||||
|
|
||||||
|
#### 3.7.1. 获取状态时间线
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/{id}/timeline/`
|
||||||
|
- **描述**: 获取一个包含所有节点及其当前状态(`not_started`, `in_progress`, `completed`, `cancelled`)的时间线。
|
||||||
|
|
||||||
|
#### 3.7.2. 获取下一个待执行节点
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/{id}/next_pending_state/`
|
||||||
|
- **描述**: 获取下一个待执行的节点信息。
|
||||||
|
- **查询参数**:
|
||||||
|
- `include_parameters`: `true` 或 `false`,是否包含节点的参数模板。
|
||||||
|
|
||||||
|
#### 3.7.3. 获取所有待执行节点
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/{id}/pending_states/`
|
||||||
|
- **描述**: 获取所有未完成的节点列表。
|
||||||
|
|
||||||
|
#### 3.7.4. 获取当前状态的参数
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/{id}/current_state_parameters/`
|
||||||
|
- **描述**: 获取当前待执行节点(`current_state`)的参数模板。
|
||||||
|
- **注意**: `current_state` 指的是下一个待执行的节点。如果流程已完成,则返回空。
|
||||||
|
|
||||||
|
### 3.8. 参数与日志接口 (Custom Actions)
|
||||||
|
|
||||||
|
#### 3.8.1. 为日志补充参数
|
||||||
|
|
||||||
|
- **POST** `/api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/add-parameters/`
|
||||||
|
- **描述**: 为某一次具体的状态流转记录(`StateFlowRecord`)补充额外的参数。
|
||||||
|
- **请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"inspector_comment": "发现轻微划痕"
|
||||||
|
},
|
||||||
|
"remark": "质检员补充"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.8.2. 获取日志的参数
|
||||||
|
|
||||||
|
- **GET** `/api/v1/stateflow/business-objects/{id}/state-logs/{log_id}/parameters/`
|
||||||
|
- **描述**: 获取某一次流转记录的所有参数。
|
||||||
|
- **查询参数**:
|
||||||
|
- `key`: 如果提供,则只返回该 `key` 的所有历史值。
|
||||||
|
- `include_cancelled`: `true` 或 `false`,是否包含已撤销的记录。
|
||||||
|
|
||||||
|
|
||||||
## 分页
|
## 分页
|
||||||
|
|
||||||
使用 LimitOffset 分页:
|
使用 LimitOffset 分页:
|
||||||
|
|||||||
@@ -214,11 +214,108 @@ class BusinessObjectAdmin(admin.ModelAdmin):
|
|||||||
return "-"
|
return "-"
|
||||||
|
|
||||||
|
|
||||||
|
class StateLogParameterRecordInline(admin.TabularInline):
|
||||||
|
"""状态流转参数记录内联显示"""
|
||||||
|
model = models.StateLogParameterRecord
|
||||||
|
extra = 0
|
||||||
|
fields = ('parameters', 'remark', 'created_at')
|
||||||
|
readonly_fields = ('created_at',)
|
||||||
|
can_delete = False
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.StateFlowRecord)
|
@admin.register(models.StateFlowRecord)
|
||||||
class StateFlowRecordAdmin(admin.ModelAdmin):
|
class StateFlowRecordAdmin(admin.ModelAdmin):
|
||||||
list_display = ('id', 'business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at')
|
list_display = ('id', 'business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at', 'parameter_count')
|
||||||
search_fields = ('business_object__name', 'state__name')
|
search_fields = ('business_object__name', 'state__name')
|
||||||
list_filter = ('state', 'is_cancelled', 'completed_at', 'completed_by')
|
list_filter = ('state', 'is_cancelled', 'completed_at', 'completed_by')
|
||||||
readonly_fields = ('business_object', 'state', 'completed_at', 'completed_by', 'cancelled_at')
|
readonly_fields = ('business_object', 'state', 'completed_at', 'completed_by', 'cancelled_at', 'parameters_summary')
|
||||||
fields = ('business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at')
|
fields = ('business_object', 'state', 'completed_at', 'completed_by', 'is_cancelled', 'cancelled_at', 'parameters_summary')
|
||||||
autocomplete_fields = ('business_object', 'state', 'completed_by')
|
autocomplete_fields = ('business_object', 'state', 'completed_by')
|
||||||
|
inlines = [StateLogParameterRecordInline]
|
||||||
|
|
||||||
|
@admin.display(description='参数记录数')
|
||||||
|
def parameter_count(self, obj):
|
||||||
|
count = obj.parameter_records.count()
|
||||||
|
return f"{count} 条" if count > 0 else "-"
|
||||||
|
|
||||||
|
@admin.display(description='参数汇总')
|
||||||
|
def parameters_summary(self, obj):
|
||||||
|
"""显示所有参数的最新值汇总"""
|
||||||
|
summary = obj.get_all_parameters_summary(include_cancelled=True)
|
||||||
|
if summary:
|
||||||
|
items = [f"{k}: {v}" for k, v in list(summary.items())[:5]]
|
||||||
|
if len(summary) > 5:
|
||||||
|
items.append(f"... 共{len(summary)}个参数")
|
||||||
|
return mark_safe('<br>'.join(items))
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.ProcessNode)
|
||||||
|
class ProcessNodeAdmin(admin.ModelAdmin):
|
||||||
|
"""流程节点关联管理"""
|
||||||
|
list_display = ('id', 'process', 'state', 'order', 'created_at')
|
||||||
|
search_fields = ('process__name', 'state__name')
|
||||||
|
list_filter = ('process', 'created_at')
|
||||||
|
autocomplete_fields = ('process', 'state')
|
||||||
|
list_editable = ('order',)
|
||||||
|
ordering = ('process', 'order')
|
||||||
|
|
||||||
|
fieldsets = [
|
||||||
|
('关联信息', {
|
||||||
|
'fields': ['process', 'state', 'order']
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.StateLogParameterRecord)
|
||||||
|
class StateLogParameterRecordAdmin(admin.ModelAdmin):
|
||||||
|
"""状态流转参数记录管理"""
|
||||||
|
list_display = ('id', 'state_log', 'parameter_keys', 'remark_preview', 'created_at')
|
||||||
|
search_fields = ('state_log__business_object__name', 'state_log__state__name', 'remark')
|
||||||
|
list_filter = ('created_at', 'state_log__state')
|
||||||
|
readonly_fields = ('created_at', 'parameter_preview')
|
||||||
|
autocomplete_fields = ('state_log',)
|
||||||
|
|
||||||
|
fieldsets = [
|
||||||
|
('关联信息', {
|
||||||
|
'fields': ['state_log']
|
||||||
|
}),
|
||||||
|
('参数数据', {
|
||||||
|
'fields': ['parameters', 'parameter_preview', 'remark']
|
||||||
|
}),
|
||||||
|
('时间信息', {
|
||||||
|
'fields': ['created_at']
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
@admin.display(description='参数键')
|
||||||
|
def parameter_keys(self, obj):
|
||||||
|
"""显示参数键列表"""
|
||||||
|
keys = obj.get_all_keys()
|
||||||
|
if keys:
|
||||||
|
preview = ', '.join(keys[:5])
|
||||||
|
if len(keys) > 5:
|
||||||
|
preview += f' 等{len(keys)}个'
|
||||||
|
return preview
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
@admin.display(description='备注')
|
||||||
|
def remark_preview(self, obj):
|
||||||
|
"""显示备注预览"""
|
||||||
|
if obj.remark:
|
||||||
|
return obj.remark[:50] + ('...' if len(obj.remark) > 50 else '')
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
@admin.display(description='参数详情')
|
||||||
|
def parameter_preview(self, obj):
|
||||||
|
"""格式化显示参数详情"""
|
||||||
|
if obj.parameters:
|
||||||
|
items = []
|
||||||
|
for key, value in obj.parameters.items():
|
||||||
|
# 如果值太长,截断显示
|
||||||
|
value_str = str(value)
|
||||||
|
if len(value_str) > 100:
|
||||||
|
value_str = value_str[:100] + '...'
|
||||||
|
items.append(f"<strong>{key}</strong>: {value_str}")
|
||||||
|
return mark_safe('<br>'.join(items))
|
||||||
|
return "-"
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ class StateFlowServicesTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_initial_state(self):
|
def test_initial_state(self):
|
||||||
"""测试初始状态 - 应该返回第一个待执行的节点"""
|
"""测试初始状态 - 应该是 None(未开始)"""
|
||||||
current_state = self.business_object.get_current_state()
|
current_state = self.business_object.get_current_state()
|
||||||
self.assertEqual(current_state.name, self.state1.name, '初始状态应该返回第一个待执行的节点')
|
self.assertIsNone(current_state, '初始状态应该是 None(未开始)')
|
||||||
self.assertEqual(self.business_object.get_progress_percentage(), 0.0)
|
self.assertEqual(self.business_object.get_progress_percentage(), 0.0)
|
||||||
|
|
||||||
# 整体状态应该是 not_started
|
# 整体状态应该是 not_started
|
||||||
@@ -57,18 +57,18 @@ class StateFlowServicesTestCase(TestCase):
|
|||||||
success, message, _ = services.advance_to_next_state(self.business_object, self.user)
|
success, message, _ = services.advance_to_next_state(self.business_object, self.user)
|
||||||
self.assertTrue(success)
|
self.assertTrue(success)
|
||||||
|
|
||||||
# current_state 应该是下一个待执行的状态(state2)
|
# current_state 应该是最后完成的状态(state1)
|
||||||
current_state = self.business_object.get_current_state()
|
current_state = self.business_object.get_current_state()
|
||||||
self.assertEqual(current_state.name, self.state2.name)
|
self.assertEqual(current_state.name, self.state1.name)
|
||||||
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 33.33, places=1)
|
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 33.33, places=1)
|
||||||
|
|
||||||
# 推进到状态2(完成state2)
|
# 推进到状态2(完成state2)
|
||||||
success, message, _ = services.advance_to_next_state(self.business_object, self.user)
|
success, message, _ = services.advance_to_next_state(self.business_object, self.user)
|
||||||
self.assertTrue(success)
|
self.assertTrue(success)
|
||||||
|
|
||||||
# current_state 应该是 state3(下一个待执行的)
|
# current_state 应该是 state2
|
||||||
current_state = self.business_object.get_current_state()
|
current_state = self.business_object.get_current_state()
|
||||||
self.assertEqual(current_state.name, self.state3.name)
|
self.assertEqual(current_state.name, self.state2.name)
|
||||||
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 66.67, places=1)
|
self.assertAlmostEqual(self.business_object.get_progress_percentage(), 66.67, places=1)
|
||||||
|
|
||||||
# 完成最后一个状态(state3)
|
# 完成最后一个状态(state3)
|
||||||
@@ -76,9 +76,9 @@ class StateFlowServicesTestCase(TestCase):
|
|||||||
self.assertTrue(success)
|
self.assertTrue(success)
|
||||||
self.assertEqual(self.business_object.get_progress_percentage(), 100.0)
|
self.assertEqual(self.business_object.get_progress_percentage(), 100.0)
|
||||||
|
|
||||||
# current_state 应该是 None(没有待执行的节点了)
|
# current_state 应该是 state3(最后完成的状态)
|
||||||
current_state = self.business_object.get_current_state()
|
current_state = self.business_object.get_current_state()
|
||||||
self.assertIsNone(current_state)
|
self.assertEqual(current_state.name, self.state3.name)
|
||||||
|
|
||||||
# 整体状态应该是 completed
|
# 整体状态应该是 completed
|
||||||
status = services.get_overall_status(self.business_object)
|
status = services.get_overall_status(self.business_object)
|
||||||
@@ -116,7 +116,7 @@ class StateFlowServicesTestCase(TestCase):
|
|||||||
|
|
||||||
timeline = self.business_object.get_timeline()
|
timeline = self.business_object.get_timeline()
|
||||||
self.assertEqual(timeline[0]['status'], 'completed')
|
self.assertEqual(timeline[0]['status'], 'completed')
|
||||||
self.assertEqual(timeline[1]['status'], 'in_progress') # 下一个待执行的节点
|
self.assertEqual(timeline[1]['status'], 'not_started')
|
||||||
self.assertEqual(timeline[2]['status'], 'not_started')
|
self.assertEqual(timeline[2]['status'], 'not_started')
|
||||||
self.assertIsNotNone(timeline[0]['completed_by'])
|
self.assertIsNotNone(timeline[0]['completed_by'])
|
||||||
self.assertEqual(timeline[0]['completed_by'].id, self.user.id)
|
self.assertEqual(timeline[0]['completed_by'].id, self.user.id)
|
||||||
@@ -141,9 +141,9 @@ class StateFlowServicesTestCase(TestCase):
|
|||||||
cancelled_count = self.business_object.state_logs.filter(is_cancelled=True).count()
|
cancelled_count = self.business_object.state_logs.filter(is_cancelled=True).count()
|
||||||
self.assertEqual(cancelled_count, 2)
|
self.assertEqual(cancelled_count, 2)
|
||||||
|
|
||||||
# 应该回到初始状态(返回第一个待执行的节点)
|
# 应该回到初始状态(未开始)
|
||||||
current_state = self.business_object.get_current_state()
|
current_state = self.business_object.get_current_state()
|
||||||
self.assertEqual(current_state.name, self.state1.name)
|
self.assertIsNone(current_state)
|
||||||
|
|
||||||
# 所有撤销的记录都应该有撤销时间
|
# 所有撤销的记录都应该有撤销时间
|
||||||
for log in self.business_object.state_logs.all():
|
for log in self.business_object.state_logs.all():
|
||||||
|
|||||||
Reference in New Issue
Block a user