docs: update material validation interface documentation to v2.0

- add User mode exclusive two-column UI layout description
- add MaterialCode deduplication logic explanation
- add hide checked items feature documentation
- update permission control (shared Production ID, dryrun configuration)
- update manager filter logic (dual-table source with deduplication)
- update deletion confirmation flow (upsert/delete separation)
- update database table documentation (upsert, get_all_records methods)
- add new FAQ entries for User mode features and dryrun
This commit is contained in:
Misaka_Company
2026-02-28 09:03:29 +08:00
parent c75e5ae52a
commit a5672def19

View File

@@ -74,10 +74,14 @@ classDiagram
class MaterialValidationTab { class MaterialValidationTab {
+start_validation() +start_validation()
+confirm_deletion() +confirm_deletion()
+start_delete_execution()
+export_results() +export_results()
-_validation_worker_enhanced() -_validation_worker_enhanced()
-_apply_manager_filter() -_apply_manager_filter()
-_sync_checkbox_by_material_code() -_sync_checkbox_by_material_code()
-_hide_checked_items()
-_show_all_items()
-_execute_sync_in_background()
} }
class CheckboxTreeview { class CheckboxTreeview {
@@ -97,6 +101,8 @@ classDiagram
class DiscreteMaterialPlanDAO { class DiscreteMaterialPlanDAO {
+query_all() +query_all()
+query_by_source_numbers() +query_by_source_numbers()
+query_all_distinct_by_material_code()
+query_by_source_numbers_distinct()
+get_unique_material_names() +get_unique_material_names()
} }
@@ -112,8 +118,10 @@ classDiagram
class MaterialsToBeDeletedDAO { class MaterialsToBeDeletedDAO {
+get_all_records() +get_all_records()
+upsert_material()
+upsert_batch() +upsert_batch()
+delete_by_material_codes() +delete_by_material_codes()
+get_managers()
} }
class SessionManager { class SessionManager {
@@ -153,10 +161,10 @@ flowchart TD
CheckMode -->|database_full| FullMode[全表校验] CheckMode -->|database_full| FullMode[全表校验]
CheckMode -->|database_filtered| FilterMode[过滤校验] CheckMode -->|database_filtered| FilterMode[过滤校验]
FullMode --> QueryAll[查询 DiscreteMaterialPlanData<br/>获取所有记录] FullMode --> QueryAll[查询 DiscreteMaterialPlanData<br/>获取所有记录并去重]
FilterMode --> ReadProductionID[读取 ProductionID.txt] FilterMode --> ReadProductionID[读取 ProductionID.txt<br/>或共享 Production ID]
ReadProductionID --> QueryContract[查询生产合同数据<br/>获取 SourceNumber] ReadProductionID --> QueryContract[查询生产合同数据<br/>获取 SourceNumber]
QueryContract --> QueryBySource[按 SourceNumber<br/>查询物料记录] QueryContract --> QueryBySource[按 SourceNumber<br/>查询物料记录并去重]
QueryAll --> FetchType[获取待删除物料类型] QueryAll --> FetchType[获取待删除物料类型]
QueryBySource --> FetchType QueryBySource --> FetchType
@@ -167,9 +175,9 @@ flowchart TD
GetMarkedRecords --> BuildDict[构建 MaterialCode->ManagerName<br/>映射字典] GetMarkedRecords --> BuildDict[构建 MaterialCode->ManagerName<br/>映射字典]
BuildDict --> Match[执行双优先级匹配] BuildDict --> Match[执行双优先级匹配]
Match --> Priority1{优先级1:<br/>精确匹配?} Match --> Priority1{优先级 1:<br/>精确匹配}
Priority1 -->|是 | SetMarked[设置 is_marked=true<br/>使用 MaterialsToBeDeleted.ManagerName] Priority1 -->|是 | SetMarked[设置 is_marked=true<br/>使用 MaterialsToBeDeleted.ManagerName]
Priority1 -->|否| Priority2{优先级2:<br/>模糊匹配?} Priority1 -->|否 | Priority2{优先级 2:<br/>模糊匹配}
Priority2 -->|是 | SetKeyword[设置 matched_keyword<br/>使用 MaterialsTypeToBeDeleted.ManagerName] Priority2 -->|是 | SetKeyword[设置 matched_keyword<br/>使用 MaterialsTypeToBeDeleted.ManagerName]
Priority2 -->|否 | SetUnmatched[设置 manager_name=null<br/>is_marked=false] Priority2 -->|否 | SetUnmatched[设置 manager_name=null<br/>is_marked=false]
@@ -223,7 +231,56 @@ flowchart LR
style P fill:#e1f5ff style P fill:#e1f5ff
``` ```
### 3.3 交互时序图 ### 3.3 MaterialCode 去重逻辑
**新增功能**: 为避免同一物料代码出现多条记录,系统实现了基于 MaterialCode 的去重机制。
**去重策略**:
- **排序规则**: `CreateDate ASC``SequenceNumber ASC`
- **保留规则**: 每个 MaterialCode 保留第一条记录
- **日志输出**: 显示移除的重复记录数量
**实现方法**:
```python
# discrete_material_plan_dao.py:390-479
def query_all_distinct_by_material_code(self) -> List[Dict]:
"""
查询所有记录,基于 MaterialCode 去重
保留策略:每个 MaterialCode 保留第一条记录
排序规则CreateDate ASC → SequenceNumber ASC
"""
with get_connection() as db:
sql = """
WITH RankedRecords AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY MaterialCode
ORDER BY CreateDate ASC, SequenceNumber ASC
) AS rn
FROM [dbo].[DiscreteMaterialPlanData]
WHERE MaterialCode IS NOT NULL
)
SELECT ... FROM RankedRecords WHERE rn = 1
"""
return db.execute_query(sql)
def query_by_source_numbers_distinct(self, source_numbers: List[str]) -> List[Dict]:
"""
按 SourceNumber 过滤查询,基于 MaterialCode 去重
"""
# 使用类似的 ROW_NUMBER() OVER (PARTITION BY ...) 语句
```
**日志示例**:
```
[INFO] 获取到 1500 条记录
[INFO] 基于 MaterialCode 去重:移除了 320 条重复记录
```
### 3.4 交互时序图
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@@ -241,13 +298,13 @@ sequenceDiagram
UI->>UI: 启动后台线程 UI->>UI: 启动后台线程
alt 全表校验模式 alt 全表校验模式
Validator->>DAO1: query_all() Validator->>DAO1: query_all_distinct_by_material_code()
DAO1-->>Validator: 返回所有物料记录 DAO1-->>Validator: 返回去重后的记录
else ProductionID 过滤模式 else ProductionID 过滤模式
Validator->>DAO2: get_source_numbers_by_总排号 () Validator->>DAO2: get_source_numbers_by_总排号 ()
DAO2-->>Validator: 返回 SourceNumber 列表 DAO2-->>Validator: 返回 SourceNumber 列表
Validator->>DAO1: query_by_source_numbers() Validator->>DAO1: query_by_source_numbers_distinct()
DAO1-->>Validator: 返回过滤后的物料记录 DAO1-->>Validator: 返回去重后的记录
end end
Validator->>DAO3: get_all_materials() Validator->>DAO3: get_all_materials()
@@ -285,12 +342,20 @@ sequenceDiagram
| `_execute_sync_in_background()` | 后台执行数据库同步操作upsert/delete | | `_execute_sync_in_background()` | 后台执行数据库同步操作upsert/delete |
| `_apply_manager_filter()` | 应用负责人筛选,更新表格显示 | | `_apply_manager_filter()` | 应用负责人筛选,更新表格显示 |
| `_sync_checkbox_by_material_code()` | 同步相同材料代码的所有记录的选择状态 | | `_sync_checkbox_by_material_code()` | 同步相同材料代码的所有记录的选择状态 |
| `_hide_checked_items()` | 隐藏所有已勾选的物料记录User 模式专属) |
| `_show_all_items()` | 显示所有隐藏的物料记录User 模式专属) |
| `_load_results_with_deletion_status()` | 加载结果并设置 checkbox 选中状态 | | `_load_results_with_deletion_status()` | 加载结果并设置 checkbox 选中状态 |
| `export_results()` | 导出结果到 Excel 文件 | | `export_results()` | 导出结果到 Excel 文件 |
| `start_delete_execution()` | 启动删除执行流程 |
| `_delete_worker()` | 后台线程执行删除操作 |
**新增属性**:
- `shared_production_ids: List[str]` - 共享的 Production ID 列表
- `hidden_items: Set[str]` - 隐藏的表格项目 ID 集合
**权限控制**: **权限控制**:
- **Admin 用户**: 可以选择数据来源(全表/过滤),可以看到所有负责人的数据,可以使用负责人筛选 - **Admin 用户**: 可以选择数据来源(全表/过滤),可以看到所有负责人的数据,可以使用负责人筛选
- **User 用户**: 只能使用 ProductionID 过滤模式,只能看到自己的数据,自动筛选到当前用户 - **User 用户**: 只能使用 ProductionID 过滤模式,只能看到自己的数据,自动筛选到当前用户,使用两列布局(表格 + 右侧按钮面板)
### 4.2 CheckboxTreeview 类 ### 4.2 CheckboxTreeview 类
@@ -319,10 +384,10 @@ sequenceDiagram
| 方法名 | 功能说明 | | 方法名 | 功能说明 |
|--------|----------| |--------|----------|
| `validate_from_database_enhanced()` | 增强的数据库校验(完整记录模式) | | `validate_from_database_enhanced()` | 增强的数据库校验(完整记录模式,支持去重 |
| `match_materials_detailed()` | 匹配物料并返回详细结果 | | `match_materials_detailed()` | 匹配物料并返回详细结果 |
| `_read_production_ids()` | 读取 ProductionID.txt 文件 | | `_read_production_ids()` | 读取 ProductionID.txt 文件 |
| `_get_source_numbers_from_production_ids()` | 通过总排号查询获取生产订单号 | | `_get_source_numbers_from_inputs()` | 通过总排号查询获取生产订单号 |
| `_get_material_names_from_db()` | 从数据库获取材料名称 | | `_get_material_names_from_db()` | 从数据库获取材料名称 |
**数据结构**: **数据结构**:
@@ -359,14 +424,14 @@ class MaterialValidationResult:
| Model | varchar | 型号 | | Model | varchar | 型号 |
| ManagerName | varchar | 负责人 | | ManagerName | varchar | 负责人 |
**查询示例**: **查询方法**:
```python ```python
# 查询所有记录 # 查询所有记录(去重)
dao = DiscreteMaterialPlanDAO() dao = DiscreteMaterialPlanDAO()
records = dao.query_all() records = dao.query_all_distinct_by_material_code()
# 按生产订单号查询 # 按生产订单号查询(去重)
records = dao.query_by_source_numbers(source_numbers) records = dao.query_by_source_numbers_distinct(source_numbers)
# 获取唯一材料名称 # 获取唯一材料名称
material_names = dao.get_unique_material_names(source_numbers) material_names = dao.get_unique_material_names(source_numbers)
@@ -385,7 +450,7 @@ material_names = dao.get_unique_material_names(source_numbers)
**匹配规则**: 如果 `DiscreteMaterialPlanData.MaterialName` 包含 `MaterialsTypeToBeDeleted.MaterialName`,则匹配成功 **匹配规则**: 如果 `DiscreteMaterialPlanData.MaterialName` 包含 `MaterialsTypeToBeDeleted.MaterialName`,则匹配成功
**查询示例**: **查询方法**:
```python ```python
dao = MaterialsTypeToBeDeletedDAO() dao = MaterialsTypeToBeDeletedDAO()
@@ -413,24 +478,40 @@ materials = dao.get_materials_by_manager('张三')
**匹配规则**: 如果 `DiscreteMaterialPlanData.MaterialCode` 等于 `MaterialsToBeDeleted.MaterialCode`,则匹配成功 **匹配规则**: 如果 `DiscreteMaterialPlanData.MaterialCode` 等于 `MaterialsToBeDeleted.MaterialCode`,则匹配成功
**操作示例**: **操作方法**:
```python ```python
dao = MaterialsToBeDeletedDAO() dao = MaterialsToBeDeletedDAO()
# 获取所有记录 # 获取所有记录(含 ID 字段)
records = dao.get_all_records() records = dao.get_all_records()
# 插入或更新单个记录
dao.upsert_material(material_code='M001', manager_name='张三')
# 批量插入/更新 # 批量插入/更新
stats = dao.upsert_batch([ stats = dao.upsert_batch([
{'material_code': 'M001', 'manager_name': '张三'}, {'material_code': 'M001', 'manager_name': '张三'},
{'material_code': 'M002', 'manager_name': '李四'} {'material_code': 'M002', 'manager_name': '李四'}
]) ])
# 返回:{'total': 2, 'success': 2, 'failed': 0}
# 按物料代码删除 # 按物料代码删除
dao.delete_by_material_code('M001') dao.delete_by_material_code('M001')
# 批量删除 # 批量删除
dao.delete_by_material_codes(['M001', 'M002', 'M003']) deleted_count = dao.delete_by_material_codes(['M001', 'M002', 'M003'])
```
**MERGE 语句实现** (SQL Server):
```sql
MERGE [dbo].[MaterialsToBeDeleted] AS target
USING (SELECT ? AS MaterialCode, ? AS ManagerName) AS source
ON (target.MaterialCode = source.MaterialCode)
WHEN MATCHED THEN
UPDATE SET ManagerName = source.ManagerName
WHEN NOT MATCHED THEN
INSERT (MaterialCode, ManagerName)
VALUES (source.MaterialCode, source.ManagerName);
``` ```
--- ---
@@ -443,22 +524,22 @@ dao.delete_by_material_codes(['M001', 'M002', 'M003'])
```mermaid ```mermaid
flowchart TD flowchart TD
Start[物料记录] --> CheckP1{优先级1:<br/>MaterialsToBeDeleted<br/>精确匹配?} Start[物料记录] --> CheckP1{优先级 1:<br/>MaterialsToBeDeleted<br/>MaterialCode 精确匹配}
CheckP1 -->|MaterialCode 精确匹配| Marked[已标记删除] CheckP1 -->|| Marked[已标记删除]
Marked --> SetM1[设置 ManagerName<br/>= MaterialsToBeDeleted.ManagerName] Marked --> SetM1[设置 ManagerName<br/>= MaterialsToBeDeleted.ManagerName]
SetM1 --> SetFlag1[is_marked_for_deletion = true] SetM1 --> SetFlag1[is_marked_for_deletion = true]
SetFlag1 --> End1[返回结果] SetFlag1 --> End1[返回结果]
CheckP1 -->|未匹配| CheckP2{优先级2:<br/>MaterialsTypeToBeDeleted<br/>模糊匹配?} CheckP1 -->|| CheckP2{优先级 2:<br/>MaterialsTypeToBeDeleted<br/>MaterialName 包含匹配}
CheckP2 -->|MaterialName 包含匹配| Keyword[匹配到关键词] CheckP2 -->|| Keyword[匹配到关键词]
Keyword --> SetM2[设置 ManagerName<br/>= MaterialsTypeToBeDeleted.ManagerName] Keyword --> SetM2[设置 ManagerName<br/>= MaterialsTypeToBeDeleted.ManagerName]
SetM2 --> SetKeyword[matched_type_keyword<br/>= 匹配的 MaterialName] SetM2 --> SetKeyword[matched_type_keyword<br/>= 匹配的 MaterialName]
SetKeyword --> SetFlag2[is_marked_for_deletion = false] SetKeyword --> SetFlag2[is_marked_for_deletion = false]
SetFlag2 --> End2[返回结果] SetFlag2 --> End2[返回结果]
CheckP2 -->|未匹配| Unmatched[未匹配] CheckP2 -->|| Unmatched[未匹配]
Unmatched --> SetNull[manager_name = null] Unmatched --> SetNull[manager_name = null]
SetNull --> SetFlag3[is_marked_for_deletion = false<br/>matched_type_keyword = null] SetNull --> SetFlag3[is_marked_for_deletion = false<br/>matched_type_keyword = null]
SetFlag3 --> End3[返回结果] SetFlag3 --> End3[返回结果]
@@ -482,9 +563,9 @@ def match_materials_detailed(
双优先级匹配算法 双优先级匹配算法
Args: Args:
material_records: DiscreteMaterialPlanData 的完整记录 material_records: DiscreteMaterialPlanData 的完整记录(已去重)
type_keywords: MaterialsTypeToBeDeleted 记录(模糊匹配) type_keywords: MaterialsTypeToBeDeleted 记录(模糊匹配)
marked_codes_dict: MaterialsToBeDeleted 的 MaterialCode->ManagerName 映射(精确匹配) marked_codes_dict: MaterialCode->ManagerName 映射(精确匹配)
Returns: Returns:
List[MaterialValidationResult]: 匹配结果列表 List[MaterialValidationResult]: 匹配结果列表
@@ -498,15 +579,11 @@ def match_materials_detailed(
model = record.get('Model', '') or None model = record.get('Model', '') or None
# ===== 优先级 1: 精确匹配 ===== # ===== 优先级 1: 精确匹配 =====
# 检查 MaterialsToBeDeleted 表MaterialCode 精确匹配)
# 这是最高优先级 - 如果 MaterialCode 存在,使用其 ManagerName
manager_name = marked_codes_dict.get(material_code) if material_code else None manager_name = marked_codes_dict.get(material_code) if material_code else None
is_marked = manager_name is not None is_marked = manager_name is not None
matched_keyword = None matched_keyword = None
# ===== 优先级 2: 模糊匹配 ===== # ===== 优先级 2: 模糊匹配 =====
# 如果不在 MaterialsToBeDeleted 中,匹配 MaterialsTypeToBeDeleted
# MaterialName 包含匹配)
if not manager_name: if not manager_name:
for type_record in type_keywords: for type_record in type_keywords:
type_material_name = type_record.get('MaterialName', '') type_material_name = type_record.get('MaterialName', '')
@@ -568,6 +645,7 @@ classDiagram
+can_select_data_source: true +can_select_data_source: true
+can_view_all_managers: true +can_view_all_managers: true
+can_use_manager_filter: true +can_use_manager_filter: true
+can_configure_dryrun: true
} }
class NormalUser { class NormalUser {
@@ -575,6 +653,7 @@ classDiagram
+can_select_data_source: false +can_select_data_source: false
+can_view_all_managers: false +can_view_all_managers: false
+can_use_manager_filter: false +can_use_manager_filter: false
+dryrun_from_config: true
} }
SessionManager --> User : manages SessionManager --> User : manages
@@ -592,8 +671,41 @@ classDiagram
| **负责人筛选** | ✅ 可使用复选框筛选 | ❌ 自动筛选到当前用户 | | **负责人筛选** | ✅ 可使用复选框筛选 | ❌ 自动筛选到当前用户 |
| **编辑负责人** | ✅ 可编辑任何人 | ✅ 可编辑任何人 | | **编辑负责人** | ✅ 可编辑任何人 | ✅ 可编辑任何人 |
| **删除确认** | ✅ 可操作所有人 | ✅ 可操作所有人 | | **删除确认** | ✅ 可操作所有人 | ✅ 可操作所有人 |
| **预览模式 (dryrun)** | ✅ 界面复选框控制 | ✅ 从配置 `execution.dryrun` 读取 |
| **界面布局** | 按钮在顶部控制面板 | 两列布局(表格 + 右侧按钮面板) |
| **专属按钮** | - | ✅ 隐藏勾选、显示全部按钮 |
### 7.3 权限检查实现 ### 7.3 共享 Production ID 管理
**代码位置**: `gui/material_validation_tab.py:57-58, 519-556`
```python
class MaterialValidationTab:
def __init__(self, ...):
self.shared_production_ids = [] # 共享的 Production ID 列表
self.hidden_items = set() # 隐藏项目跟踪
def on_production_ids_updated(self, production_ids: list):
"""当数据提取页面的 Production ID 更新时调用"""
self.shared_production_ids = production_ids
is_user_only = not self.session_manager.is_admin()
# User 模式:静默更新,不显示提示
if is_user_only:
if production_ids and hasattr(self, "execute_delete_button"):
self.execute_delete_button.config(state=tk.NORMAL)
return
# Admin 模式:显示共享 Production ID 预览提示
if production_ids:
count = len(production_ids)
preview = ", ".join(production_ids[:3])
if count > 3:
preview += f" ... (共 {count} 个)"
self.shared_ids_info_label.config(text=f"📋 {preview}")
```
### 7.4 权限检查实现
```python ```python
# 检查是否为管理员 # 检查是否为管理员
@@ -605,11 +717,11 @@ if self.session_manager.is_admin():
else: else:
# 普通用户默认设置 # 普通用户默认设置
self.source_mode = tk.StringVar(value="database_filtered") self.source_mode = tk.StringVar(value="database_filtered")
# 隐藏管理员专属控件 # User 模式:两列布局
manager_filter_frame = ttk.Frame(...) self._create_user_filter_buttons(main_result_container)
``` ```
### 7.4 数据过滤逻辑 ### 7.5 数据过滤逻辑
```python ```python
def _get_selected_managers(self) -> List[str]: def _get_selected_managers(self) -> List[str]:
@@ -641,14 +753,14 @@ flowchart TD
GetItem --> GetCode[获取该行的 MaterialCode] GetItem --> GetCode[获取该行的 MaterialCode]
GetCode --> Iterate[遍历表格所有行] GetCode --> Iterate[遍历表格所有行]
Iterate --> CheckCode{MaterialCode<br/>相同?} Iterate --> CheckCode{MaterialCode<br/>相同}
CheckCode -->|是| CheckState{状态<br/>不同?} CheckCode -->|是 | CheckState{状态<br/>不同}
CheckState -->|是 | Update[更新 checkbox 状态] CheckState -->|是 | Update[更新 checkbox 状态]
CheckState -->|否 | Next[继续下一行] CheckState -->|否 | Next[继续下一行]
CheckCode -->|否 | Next CheckCode -->|否 | Next
Update --> Next Update --> Next
Next --> MoreRows{还有行?} Next --> MoreRows{还有行}
MoreRows -->|是 | Iterate MoreRows -->|是 | Iterate
MoreRows -->|否 | Log[记录同步数量] MoreRows -->|否 | Log[记录同步数量]
Log --> End[完成] Log --> End[完成]
@@ -690,18 +802,21 @@ def _sync_checkbox_by_material_code(self, changed_item: str, new_state: bool):
**功能**: 管理员可以通过复选框筛选显示特定负责人的物料记录。 **功能**: 管理员可以通过复选框筛选显示特定负责人的物料记录。
**界面布局**: **负责人列表来源**:
- `MaterialsToBeDeletedDAO.get_managers()` - 已标记删除记录表
- `MaterialsTypeToBeDeletedDAO.get_managers()` - 物料类型表
- 合并后去重排序
**界面布局** (左右两列):
``` ```
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
│ 筛选(按负责人) │ │ 筛选(按负责人) │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
☑ 全选 ☑ 张三 ☑ 李四 ☑ 王五 ☑ 赵六 ☑ 钱七 ┌─────────────┐ ☑ 张三 ☑ 李四 ☑ 王五 ☑ 赵六 ☑ 钱七 ☑ 孙八
☑ 孙八 ☑ 周九 ☑ 吴十 ☑ 郑十一 ☑ 陈十二 ☑ 沈十三 │ 全选 │ ☑ 周九 ☑ 吴十 ☑ 郑十一 ☑ 陈十二 ☑ 沈十三 │
│ 取消全选 │
┌─────────┐ ┌──────────┐ └─────────────┘
│ │ 全选 │ │ 取消全选 │ │
│ └─────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
@@ -718,7 +833,10 @@ def _apply_manager_filter(self):
if not self.material_records_cache: if not self.material_records_cache:
return return
# 获取选中的负责人列表 # PERMISSION CHECK: 非管理员用户强制筛选到当前用户
if not self.session_manager.is_admin():
selected_managers = [self.session_manager.get_username()]
else:
selected_managers = self._get_selected_managers() selected_managers = self._get_selected_managers()
if not selected_managers: if not selected_managers:
@@ -735,12 +853,16 @@ def _apply_manager_filter(self):
] ]
self._refresh_filtered_results(filtered_records) self._refresh_filtered_results(filtered_records)
self._update_log(f"筛选结果:共 {len(filtered_records)} 条记录", "INFO") self._update_log(
f"筛选结果:共 {len(filtered_records)} 条记录"
f"(其中 {empty_manager_count} 条负责人为空,待编辑)",
"INFO"
)
``` ```
### 8.3 表格编辑 ### 8.3 表格编辑
**功能**: 支持双击"负责人"单元格进行编辑。 **功能**: 双击"负责人"单元格进行编辑。
**编辑流程**: **编辑流程**:
@@ -768,7 +890,6 @@ sequenceDiagram
```python ```python
def _on_cell_double_click(self, event): def _on_cell_double_click(self, event):
"""处理单元格双击事件,编辑负责人""" """处理单元格双击事件,编辑负责人"""
# 获取点击位置
region = self.tree.identify_region(event.x, event.y) region = self.tree.identify_region(event.x, event.y)
if region == "cell": if region == "cell":
@@ -788,14 +909,84 @@ def _on_cell_double_click(self, event):
) )
if new_value is not None: # 用户没有取消 if new_value is not None: # 用户没有取消
# 更新单元格值
new_values = list(values) new_values = list(values)
new_values[5] = new_value new_values[5] = new_value
self.tree.item(item, values=new_values) self.tree.item(item, values=new_values)
self.log_text.info(f"已更新负责人:{current_value} -> {new_value}")
self.log_text.info(f"已更新负责人: {current_value} -> {new_value}")
``` ```
### 8.4 隐藏勾选物料功能User 模式专属)
**功能**: 隐藏已勾选的物料记录,方便用户专注处理未勾选的物料。
**代码位置**: `gui/material_validation_tab.py:433-459`
```python
def _hide_checked_items(self):
"""隐藏所有已勾选的物料"""
checked_items = self.tree.get_checked_items()
if not checked_items:
self.log_text.info("没有已勾选的物料")
return
for item in checked_items:
self.tree.detach(item) # Remove from view but keep data
self.hidden_items.add(item)
self.log_text.info(f"已隐藏 {len(checked_items)} 条勾选的物料")
self.btn_show_all.config(state=tk.NORMAL)
def _show_all_items(self):
"""显示所有物料(包括已隐藏的)"""
if not self.hidden_items:
self.log_text.info("没有隐藏的物料")
return
for item in self.hidden_items:
self.tree.move(item, "", "end") # Restore to end of tree
count = len(self.hidden_items)
self.hidden_items.clear()
self.log_text.info(f"已显示 {count} 条隐藏的物料")
```
**使用场景**:
1. User 模式下,用户勾选了已处理完成的物料
2. 点击"隐藏勾选"按钮,隐藏已处理记录
3. 专注处理剩余的未勾选物料
4. 完成后点击"显示全部"恢复显示
### 8.5 User 模式专属 UI 布局
**代码位置**: `gui/material_validation_tab.py:260-431`
**布局对比**:
| 模式 | 布局描述 |
|------|----------|
| **Admin** | 表格占满整个区域,按钮在顶部控制面板 |
| **User** | 两列布局:左侧表格 + 右侧按钮面板 |
**User 模式界面布局**:
```
┌────────────────────────────────────────────────────────────┐
│ 校验结果 │
├───────────────────────────┬────────────────────────────────┤
│ 表格区域 │ 隐藏勾选 │
│ ┌─────────────────────┐ │ 显示全部 │
│ │ ☐ 物料 1 ... │ │ ──────────── │
│ │ ☑ 物料 2 ... │ │ 全选 │
│ │ ☐ 物料 3 ... │ │ 取消全选 │
│ │ ☐ 物料 4 ... │ │ 确认删除 │
│ └─────────────────────┘ │ 执行删除 │
└───────────────────────────┴────────────────────────────────┘
```
**按钮可见性**:
- **隐藏勾选**、**显示全部**: 仅 User 模式可见
- **全选**、**取消全选**、**确认删除**、**执行删除**: Admin 模式在顶部User 模式在右侧
--- ---
## 9. 使用场景 ## 9. 使用场景
@@ -807,7 +998,7 @@ def _on_cell_double_click(self, event):
**操作步骤**: **操作步骤**:
1. 选择"数据库 - 全表校验"模式 1. 选择"数据库 - 全表校验"模式
2. 点击"开始校验" 2. 点击"开始校验"
3. 系统查询整个 `DiscreteMaterialPlanData` 3. 系统查询整个 `DiscreteMaterialPlanData`(自动去重)
4. 使用负责人筛选器筛选特定负责人 4. 使用负责人筛选器筛选特定负责人
5. 双击编辑负责人 5. 双击编辑负责人
6. 勾选需要删除的记录 6. 勾选需要删除的记录
@@ -820,11 +1011,12 @@ def _on_cell_double_click(self, event):
**操作步骤**: **操作步骤**:
1. 在"数据提取"页面输入 ProductionID 1. 在"数据提取"页面输入 ProductionID
2. 切换到"物料校验"标签页 2. 切换到"物料校验"标签页
3. 系统自动使用共享的 ProductionID 3. 系统自动使用共享的 Production ID(静默更新,无提示)
4. 点击"开始校验" 4. 点击"开始校验"(自动使用 ProductionID 过滤模式)
5. 仅显示当前用户负责的物料 5. 仅显示当前用户负责的物料
6. 编辑负责人或勾选删除 6. 可使用"隐藏勾选"功能隐藏已处理记录
7. 点击"确认删除"保存 7. 编辑负责人或勾选删除
8. 点击"确认删除"保存
### 9.3 场景三:批量删除确认 ### 9.3 场景三:批量删除确认
@@ -845,7 +1037,7 @@ flowchart TD
Edit --> Confirm Edit --> Confirm
Validate -->|全部完整 | ShowConfirm[显示确认对话框] Validate -->|全部完整 | ShowConfirm[显示确认对话框]
ShowConfirm --> UserConfirm{用户确认?} ShowConfirm --> UserConfirm{用户确认}
UserConfirm -->|否 | Cancel[取消操作] UserConfirm -->|否 | Cancel[取消操作]
UserConfirm -->|是 | Execute[执行后台同步] UserConfirm -->|是 | Execute[执行后台同步]
@@ -867,6 +1059,96 @@ flowchart TD
style Complete fill:#e1ffe1 style Complete fill:#e1ffe1
``` ```
**删除确认流程代码**:
```python
def confirm_deletion(self):
"""确认删除主流程"""
# 收集所有数据
to_upsert = [] # 需要写入/更新的记录
to_delete = [] # 需要删除的记录
missing_manager = [] # 缺少负责人的记录
for item in self.tree.get_children():
values = self.tree.item(item, "values")
is_checked = self.tree.checkboxes.get(item, False)
material_code = values[2]
manager_name = values[5]
if is_checked:
# 已勾选:需要写入/更新
if not manager_name or not manager_name.strip():
missing_manager.append(material_code)
else:
to_upsert.append({
'material_code': material_code,
'manager_name': manager_name.strip()
})
else:
# 未勾选:需要删除
to_delete.append(material_code)
# 验证:已勾选的记录必须有负责人
if missing_manager:
messagebox.showwarning("警告", "以下记录缺少负责人信息...")
return
# 在后台线程中执行
threading.Thread(
target=self._execute_sync_in_background,
args=(to_upsert, to_delete),
daemon=True
).start()
```
**统计反馈**:
```
同步操作完成!
写入/更新成功15 条
删除成功8 条
```
### 9.4 场景四:执行删除(后台任务)
**需求**: 执行实际的删除操作,清理 ERP 系统中的数据
**操作流程**:
```python
def start_delete_execution(self):
"""开始执行删除"""
# 1. 获取 Production ID
# 2. 获取负责人列表
# 3. 获取 dryrun 设置
# 4. 确认执行
# 5. 创建进度窗口 DeleteProgressWindow
# 6. 启动后台线程 _delete_worker
def _delete_worker(self, production_ids, manager_names, dryrun):
"""后台线程执行删除"""
from utils.discrete_material_plan_cleaner import DiscreteMaterialPlanCleaner
cleaner = DiscreteMaterialPlanCleaner(
username=...,
password=...,
manager_names=manager_names,
dryrun=dryrun,
save_report=True,
progress_callback=progress_callback
)
cleaner.clean(temp_file)
# 生成报告
report = cleaner.generate_report()
self._delete_complete(report, cleaner.stats)
```
**进度窗口**:
- 使用 `DeleteProgressWindow` 显示进度
- 支持取消操作
- 生成 Markdown 格式报告
--- ---
## 10. 技术要点 ## 10. 技术要点
@@ -895,7 +1177,6 @@ def _update_log(self, message: str, level: str = "INFO"):
self.log_text.info(message) self.log_text.info(message)
elif level == "ERROR": elif level == "ERROR":
self.log_text.error(message) self.log_text.error(message)
# ...
self.after(0, update) # 在主线程中执行 self.after(0, update) # 在主线程中执行
``` ```
@@ -938,13 +1219,42 @@ def _batch_insert(self, db, df: pd.DataFrame, batch_size: int = 72) -> int:
return total_inserted return total_inserted
``` ```
### 10.4 临时文件管理
使用共享 Production ID 时,创建临时文件供校验器使用:
```python
def _validation_worker_enhanced(self, ...):
temp_production_id_file = None
try:
# 如果提供了共享的 Production ID 列表,创建临时文件
if production_ids_list:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
temp_production_id_file = f.name
f.write("\n".join(production_ids_list))
production_id_file = temp_production_id_file
# ... 执行校验 ...
finally:
# 清理临时文件
if temp_production_id_file and os.path.exists(temp_production_id_file):
os.unlink(temp_production_id_file)
```
--- ---
## 11. 常见问题 ## 11. 常见问题
### Q1: 为什么同一个 MaterialCode 会有多条记录? ### Q1: 为什么同一个 MaterialCode 会有多条记录?
**A**: 因为同一个物料代码可能出现在不同的备料计划单号或生产订单号中。复选框同步机制确保了相同 MaterialCode 的记录会被一起选中/取消。 **A**: 因为同一个物料代码可能出现在不同的备料计划单号或生产订单号中。系统现已实现 MaterialCode 去重机制:
- 全表校验时自动去重
- ProductionID 过滤时自动去重
- 复选框同步机制确保相同 MaterialCode 的记录会被一起选中/取消
### Q2: 已标记删除is_marked=true和匹配到关键词有什么区别 ### Q2: 已标记删除is_marked=true和匹配到关键词有什么区别
@@ -966,6 +1276,22 @@ def _batch_insert(self, db, df: pd.DataFrame, batch_size: int = 72) -> int:
1. **优先级 1精确匹配**: 如果 `MaterialCode``MaterialsToBeDeleted` 表中存在,使用该表的 `ManagerName`,并标记为 `is_marked=true` 1. **优先级 1精确匹配**: 如果 `MaterialCode``MaterialsToBeDeleted` 表中存在,使用该表的 `ManagerName`,并标记为 `is_marked=true`
2. **优先级 2模糊匹配**: 如果优先级 1 未匹配,检查 `MaterialName` 是否包含 `MaterialsTypeToBeDeleted` 表中的任何 `MaterialName`,如果包含,使用该表的 `ManagerName` 2. **优先级 2模糊匹配**: 如果优先级 1 未匹配,检查 `MaterialName` 是否包含 `MaterialsTypeToBeDeleted` 表中的任何 `MaterialName`,如果包含,使用该表的 `ManagerName`
### Q6: User 模式的"隐藏勾选"功能如何使用?
**A**:
1. 勾选已处理完成的物料记录
2. 点击右侧按钮面板的"隐藏勾选"按钮
3. 已勾选的记录会从表格中隐藏(数据仍保留)
4. 点击"显示全部"可恢复显示所有记录
### Q7: 预览模式 (dryrun) 如何工作?
**A**:
- **Admin 用户**: 通过界面复选框控制
- **User 用户**: 从配置文件 `execution.dryrun` 读取
- 预览模式下,删除操作不会实际保存更改
- 用于测试和验证删除逻辑
--- ---
## 12. 扩展建议 ## 12. 扩展建议
@@ -973,19 +1299,30 @@ def _batch_insert(self, db, df: pd.DataFrame, batch_size: int = 72) -> int:
### 12.1 性能优化 ### 12.1 性能优化
- 对于大数据量的全表查询,考虑添加分页功能 - 对于大数据量的全表查询,考虑添加分页功能
- 实现查询结果缓存,减少重复查询 - 实现查询结果缓存,减少重复查询
- 优化 MaterialCode 去重算法性能
### 12.2 功能增强 ### 12.2 功能增强
- 添加批量导入功能,支持从 Excel 导入负责人信息 - 添加批量导入功能,支持从 Excel 导入负责人信息
- 实现导出模板功能,方便离线编辑 - 实现导出模板功能,方便离线编辑
- 添加校验历史记录,追溯修改历史 - 添加校验历史记录,追溯修改历史
- 支持按规格、型号筛选物料
### 12.3 用户体验 ### 12.3 用户体验
- 实现拖拽排序功能 - 实现拖拽排序功能
- 添加列筛选和排序功能 - 添加列筛选和排序功能
- 支持自定义列显示/隐藏 - 支持自定义列显示/隐藏
- 增加搜索功能,快速定位物料
--- ---
**文档版本**: 1.0 **文档版本**: 2.0
**最后更新**: 2026-02-24 **最后更新**: 2026-02-28
**维护者**: Development Team **维护者**: Development Team
**更新说明**:
- 新增 User 模式专属 UI 布局说明
- 新增 MaterialCode 去重逻辑说明
- 新增隐藏勾选物料功能说明
- 更新权限控制说明(共享 Production ID、dryrun 配置)
- 更新负责人筛选逻辑(双表获取、去重)
- 更新删除确认流程upsert/delete 分离)
- 更新数据库表说明upsert、get_all_records 等方法)