diff --git a/docs/MATERIAL_VALIDATION_INTERFACE.md b/docs/MATERIAL_VALIDATION_INTERFACE.md
index a80ec0f..5e6e043 100644
--- a/docs/MATERIAL_VALIDATION_INTERFACE.md
+++ b/docs/MATERIAL_VALIDATION_INTERFACE.md
@@ -20,7 +20,7 @@ auth/session_manager.py - 会话管理(权限控制)
- **DiscreteMaterialPlanData** - 离散备料计划数据表(主数据源)
- **MaterialsTypeToBeDeleted** - 待删除物料类型表(按物料名称匹配)
- **MaterialsToBeDeleted** - 已标记删除的物料记录表(按物料代码精确匹配)
-- **[productionContractData].[26年压力表合同数据]** - 生产合同数据表
+- **[productionContractData].[26 年压力表合同数据]** - 生产合同数据表
---
@@ -38,7 +38,7 @@ graph TD
D --> G[校验模式选择]
G --> H[database_full
全表校验]
- G --> I[database_filtered
ProductionID过滤]
+ G --> I[database_filtered
ProductionID 过滤]
H --> J[MaterialStatusValidator]
I --> J
@@ -49,7 +49,7 @@ graph TD
J --> N[MaterialsToBeDeletedDAO]
K --> O[(DiscreteMaterialPlanData)]
- L --> P[(26年压力表合同数据)]
+ L --> P[(26 年压力表合同数据)]
M --> Q[(MaterialsTypeToBeDeleted)]
N --> R[(MaterialsToBeDeleted)]
@@ -74,10 +74,14 @@ classDiagram
class MaterialValidationTab {
+start_validation()
+confirm_deletion()
+ +start_delete_execution()
+export_results()
-_validation_worker_enhanced()
-_apply_manager_filter()
-_sync_checkbox_by_material_code()
+ -_hide_checked_items()
+ -_show_all_items()
+ -_execute_sync_in_background()
}
class CheckboxTreeview {
@@ -97,12 +101,14 @@ classDiagram
class DiscreteMaterialPlanDAO {
+query_all()
+query_by_source_numbers()
+ +query_all_distinct_by_material_code()
+ +query_by_source_numbers_distinct()
+get_unique_material_names()
}
class ProductionContractDataDAO {
- +get_source_numbers_by_总排号()
- +query_by_总排号()
+ +get_source_numbers_by_总排号 ()
+ +query_by_总排号 ()
}
class MaterialsTypeToBeDeletedDAO {
@@ -112,8 +118,10 @@ classDiagram
class MaterialsToBeDeletedDAO {
+get_all_records()
+ +upsert_material()
+upsert_batch()
+delete_by_material_codes()
+ +get_managers()
}
class SessionManager {
@@ -153,10 +161,10 @@ flowchart TD
CheckMode -->|database_full| FullMode[全表校验]
CheckMode -->|database_filtered| FilterMode[过滤校验]
- FullMode --> QueryAll[查询 DiscreteMaterialPlanData
获取所有记录]
- FilterMode --> ReadProductionID[读取 ProductionID.txt]
+ FullMode --> QueryAll[查询 DiscreteMaterialPlanData
获取所有记录并去重]
+ FilterMode --> ReadProductionID[读取 ProductionID.txt
或共享 Production ID]
ReadProductionID --> QueryContract[查询生产合同数据
获取 SourceNumber]
- QueryContract --> QueryBySource[按 SourceNumber
查询物料记录]
+ QueryContract --> QueryBySource[按 SourceNumber
查询物料记录并去重]
QueryAll --> FetchType[获取待删除物料类型]
QueryBySource --> FetchType
@@ -167,12 +175,12 @@ flowchart TD
GetMarkedRecords --> BuildDict[构建 MaterialCode->ManagerName
映射字典]
BuildDict --> Match[执行双优先级匹配]
- Match --> Priority1{优先级1:
精确匹配?}
- Priority1 -->|是| SetMarked[设置 is_marked=true
使用 MaterialsToBeDeleted.ManagerName]
- Priority1 -->|否| Priority2{优先级2:
模糊匹配?}
+ Match --> Priority1{优先级 1:
精确匹配?}
+ Priority1 -->|是 | SetMarked[设置 is_marked=true
使用 MaterialsToBeDeleted.ManagerName]
+ Priority1 -->|否 | Priority2{优先级 2:
模糊匹配?}
- Priority2 -->|是| SetKeyword[设置 matched_keyword
使用 MaterialsTypeToBeDeleted.ManagerName]
- Priority2 -->|否| SetUnmatched[设置 manager_name=null
is_marked=false]
+ Priority2 -->|是 | SetKeyword[设置 matched_keyword
使用 MaterialsTypeToBeDeleted.ManagerName]
+ Priority2 -->|否 | SetUnmatched[设置 manager_name=null
is_marked=false]
SetMarked --> CreateResult[创建 MaterialValidationResult]
SetKeyword --> CreateResult
@@ -194,7 +202,7 @@ flowchart TD
```mermaid
flowchart LR
A[ProductionID.txt
总排号列表] --> B[ProductionContractDataDAO]
- B --> C[[26年压力表合同数据]]
+ B --> C[[26 年压力表合同数据]]
C --> D[SourceNumber
生产订单号]
D --> E[DiscreteMaterialPlanDAO]
@@ -223,7 +231,56 @@ flowchart LR
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
sequenceDiagram
@@ -241,13 +298,13 @@ sequenceDiagram
UI->>UI: 启动后台线程
alt 全表校验模式
- Validator->>DAO1: query_all()
- DAO1-->>Validator: 返回所有物料记录
- else ProductionID过滤模式
- Validator->>DAO2: get_source_numbers_by_总排号()
+ Validator->>DAO1: query_all_distinct_by_material_code()
+ DAO1-->>Validator: 返回去重后的记录
+ else ProductionID 过滤模式
+ Validator->>DAO2: get_source_numbers_by_总排号 ()
DAO2-->>Validator: 返回 SourceNumber 列表
- Validator->>DAO1: query_by_source_numbers()
- DAO1-->>Validator: 返回过滤后的物料记录
+ Validator->>DAO1: query_by_source_numbers_distinct()
+ DAO1-->>Validator: 返回去重后的记录
end
Validator->>DAO3: get_all_materials()
@@ -285,12 +342,20 @@ sequenceDiagram
| `_execute_sync_in_background()` | 后台执行数据库同步操作(upsert/delete) |
| `_apply_manager_filter()` | 应用负责人筛选,更新表格显示 |
| `_sync_checkbox_by_material_code()` | 同步相同材料代码的所有记录的选择状态 |
+| `_hide_checked_items()` | 隐藏所有已勾选的物料记录(User 模式专属) |
+| `_show_all_items()` | 显示所有隐藏的物料记录(User 模式专属) |
| `_load_results_with_deletion_status()` | 加载结果并设置 checkbox 选中状态 |
| `export_results()` | 导出结果到 Excel 文件 |
+| `start_delete_execution()` | 启动删除执行流程 |
+| `_delete_worker()` | 后台线程执行删除操作 |
+
+**新增属性**:
+- `shared_production_ids: List[str]` - 共享的 Production ID 列表
+- `hidden_items: Set[str]` - 隐藏的表格项目 ID 集合
**权限控制**:
- **Admin 用户**: 可以选择数据来源(全表/过滤),可以看到所有负责人的数据,可以使用负责人筛选
-- **User 用户**: 只能使用 ProductionID 过滤模式,只能看到自己的数据,自动筛选到当前用户
+- **User 用户**: 只能使用 ProductionID 过滤模式,只能看到自己的数据,自动筛选到当前用户,使用两列布局(表格 + 右侧按钮面板)
### 4.2 CheckboxTreeview 类
@@ -319,10 +384,10 @@ sequenceDiagram
| 方法名 | 功能说明 |
|--------|----------|
-| `validate_from_database_enhanced()` | 增强的数据库校验(完整记录模式) |
+| `validate_from_database_enhanced()` | 增强的数据库校验(完整记录模式,支持去重) |
| `match_materials_detailed()` | 匹配物料并返回详细结果 |
| `_read_production_ids()` | 读取 ProductionID.txt 文件 |
-| `_get_source_numbers_from_production_ids()` | 通过总排号查询获取生产订单号 |
+| `_get_source_numbers_from_inputs()` | 通过总排号查询获取生产订单号 |
| `_get_material_names_from_db()` | 从数据库获取材料名称 |
**数据结构**:
@@ -359,14 +424,14 @@ class MaterialValidationResult:
| Model | varchar | 型号 |
| ManagerName | varchar | 负责人 |
-**查询示例**:
+**查询方法**:
```python
-# 查询所有记录
+# 查询所有记录(去重)
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)
@@ -385,7 +450,7 @@ material_names = dao.get_unique_material_names(source_numbers)
**匹配规则**: 如果 `DiscreteMaterialPlanData.MaterialName` 包含 `MaterialsTypeToBeDeleted.MaterialName`,则匹配成功
-**查询示例**:
+**查询方法**:
```python
dao = MaterialsTypeToBeDeletedDAO()
@@ -413,24 +478,40 @@ materials = dao.get_materials_by_manager('张三')
**匹配规则**: 如果 `DiscreteMaterialPlanData.MaterialCode` 等于 `MaterialsToBeDeleted.MaterialCode`,则匹配成功
-**操作示例**:
+**操作方法**:
```python
dao = MaterialsToBeDeletedDAO()
-# 获取所有记录
+# 获取所有记录(含 ID 字段)
records = dao.get_all_records()
+# 插入或更新单个记录
+dao.upsert_material(material_code='M001', manager_name='张三')
+
# 批量插入/更新
stats = dao.upsert_batch([
{'material_code': 'M001', 'manager_name': '张三'},
{'material_code': 'M002', 'manager_name': '李四'}
])
+# 返回:{'total': 2, 'success': 2, 'failed': 0}
# 按物料代码删除
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
flowchart TD
- Start[物料记录] --> CheckP1{优先级1:
MaterialsToBeDeleted
精确匹配?}
+ Start[物料记录] --> CheckP1{优先级 1:
MaterialsToBeDeleted
MaterialCode 精确匹配?}
- CheckP1 -->|MaterialCode 精确匹配| Marked[已标记删除]
+ CheckP1 -->|是 | Marked[已标记删除]
Marked --> SetM1[设置 ManagerName
= MaterialsToBeDeleted.ManagerName]
SetM1 --> SetFlag1[is_marked_for_deletion = true]
SetFlag1 --> End1[返回结果]
- CheckP1 -->|未匹配| CheckP2{优先级2:
MaterialsTypeToBeDeleted
模糊匹配?}
+ CheckP1 -->|否 | CheckP2{优先级 2:
MaterialsTypeToBeDeleted
MaterialName 包含匹配?}
- CheckP2 -->|MaterialName 包含匹配| Keyword[匹配到关键词]
+ CheckP2 -->|是 | Keyword[匹配到关键词]
Keyword --> SetM2[设置 ManagerName
= MaterialsTypeToBeDeleted.ManagerName]
SetM2 --> SetKeyword[matched_type_keyword
= 匹配的 MaterialName]
SetKeyword --> SetFlag2[is_marked_for_deletion = false]
SetFlag2 --> End2[返回结果]
- CheckP2 -->|未匹配| Unmatched[未匹配]
+ CheckP2 -->|否 | Unmatched[未匹配]
Unmatched --> SetNull[manager_name = null]
SetNull --> SetFlag3[is_marked_for_deletion = false
matched_type_keyword = null]
SetFlag3 --> End3[返回结果]
@@ -482,9 +563,9 @@ def match_materials_detailed(
双优先级匹配算法
Args:
- material_records: DiscreteMaterialPlanData 的完整记录
+ material_records: DiscreteMaterialPlanData 的完整记录(已去重)
type_keywords: MaterialsTypeToBeDeleted 记录(模糊匹配)
- marked_codes_dict: MaterialsToBeDeleted 的 MaterialCode->ManagerName 映射(精确匹配)
+ marked_codes_dict: MaterialCode->ManagerName 映射(精确匹配)
Returns:
List[MaterialValidationResult]: 匹配结果列表
@@ -498,15 +579,11 @@ def match_materials_detailed(
model = record.get('Model', '') or None
# ===== 优先级 1: 精确匹配 =====
- # 检查 MaterialsToBeDeleted 表(MaterialCode 精确匹配)
- # 这是最高优先级 - 如果 MaterialCode 存在,使用其 ManagerName
manager_name = marked_codes_dict.get(material_code) if material_code else None
is_marked = manager_name is not None
matched_keyword = None
# ===== 优先级 2: 模糊匹配 =====
- # 如果不在 MaterialsToBeDeleted 中,匹配 MaterialsTypeToBeDeleted
- # (MaterialName 包含匹配)
if not manager_name:
for type_record in type_keywords:
type_material_name = type_record.get('MaterialName', '')
@@ -533,10 +610,10 @@ def match_materials_detailed(
| MaterialCode | MaterialName | MaterialsToBeDeleted | MaterialsTypeToBeDeleted | 匹配结果 | is_marked | ManagerName |
|--------------|--------------|----------------------|--------------------------|----------|-----------|-------------|
-| M001 | 螺栓 M8×20 | M001 → 张三 | - | 优先级1精确匹配 | true | 张三 |
-| M002 | 垫圈 Φ8 | - | 垫圈 → 李四 | 优先级2模糊匹配 | false | 李四 |
+| M001 | 螺栓 M8×20 | M001 → 张三 | - | 优先级 1 精确匹配 | true | 张三 |
+| M002 | 垫圈 Φ8 | - | 垫圈 → 李四 | 优先级 2 模糊匹配 | false | 李四 |
| M003 | 螺母 M6 | - | - | 未匹配 | false | null |
-| M004 | 不锈钢螺栓 M10×30 | M004 → 王五 | 螺栓 → 赵六 | 优先级1精确匹配(忽略螺栓) | true | 王五 |
+| M004 | 不锈钢螺栓 M10×30 | M004 → 王五 | 螺栓 → 赵六 | 优先级 1 精确匹配(忽略螺栓) | true | 王五 |
---
@@ -568,6 +645,7 @@ classDiagram
+can_select_data_source: true
+can_view_all_managers: true
+can_use_manager_filter: true
+ +can_configure_dryrun: true
}
class NormalUser {
@@ -575,6 +653,7 @@ classDiagram
+can_select_data_source: false
+can_view_all_managers: false
+can_use_manager_filter: false
+ +dryrun_from_config: true
}
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
# 检查是否为管理员
@@ -605,11 +717,11 @@ if self.session_manager.is_admin():
else:
# 普通用户默认设置
self.source_mode = tk.StringVar(value="database_filtered")
- # 隐藏管理员专属控件
- manager_filter_frame = ttk.Frame(...)
+ # User 模式:两列布局
+ self._create_user_filter_buttons(main_result_container)
```
-### 7.4 数据过滤逻辑
+### 7.5 数据过滤逻辑
```python
def _get_selected_managers(self) -> List[str]:
@@ -641,16 +753,16 @@ flowchart TD
GetItem --> GetCode[获取该行的 MaterialCode]
GetCode --> Iterate[遍历表格所有行]
- Iterate --> CheckCode{MaterialCode
相同?}
- CheckCode -->|是| CheckState{状态
不同?}
- CheckState -->|是| Update[更新 checkbox 状态]
- CheckState -->|否| Next[继续下一行]
- CheckCode -->|否| Next
+ Iterate --> CheckCode{MaterialCode
相同?}
+ CheckCode -->|是 | CheckState{状态
不同?}
+ CheckState -->|是 | Update[更新 checkbox 状态]
+ CheckState -->|否 | Next[继续下一行]
+ CheckCode -->|否 | Next
Update --> Next
- Next --> MoreRows{还有行?}
- MoreRows -->|是| Iterate
- MoreRows -->|否| Log[记录同步数量]
+ Next --> MoreRows{还有行?}
+ MoreRows -->|是 | Iterate
+ MoreRows -->|否 | Log[记录同步数量]
Log --> End[完成]
style UserClick fill:#e1f5ff
@@ -667,7 +779,7 @@ def _sync_checkbox_by_material_code(self, changed_item: str, new_state: bool):
values = self.tree.item(changed_item, "values")
if not values or len(values) <= 2:
return
- material_code = values[2] # 材料代码在第3列(索引2)
+ material_code = values[2] # 材料代码在第 3 列(索引 2)
# 同步所有具有相同材料代码的记录
synced_count = 0
@@ -690,18 +802,21 @@ def _sync_checkbox_by_material_code(self, changed_item: str, new_state: bool):
**功能**: 管理员可以通过复选框筛选显示特定负责人的物料记录。
-**界面布局**:
+**负责人列表来源**:
+- `MaterialsToBeDeletedDAO.get_managers()` - 已标记删除记录表
+- `MaterialsTypeToBeDeletedDAO.get_managers()` - 物料类型表
+- 合并后去重排序
+
+**界面布局** (左右两列):
```
┌─────────────────────────────────────────────────────────────┐
│ 筛选(按负责人) │
├─────────────────────────────────────────────────────────────┤
-│ ☑ 全选 ☑ 张三 ☑ 李四 ☑ 王五 ☑ 赵六 ☑ 钱七 │
-│ ☑ 孙八 ☑ 周九 ☑ 吴十 ☑ 郑十一 ☑ 陈十二 ☑ 沈十三 │
-│ │
-│ ┌─────────┐ ┌──────────┐ │
-│ │ 全选 │ │ 取消全选 │ │
-│ └─────────┘ └──────────┘ │
+│ ┌─────────────┐ ☑ 张三 ☑ 李四 ☑ 王五 ☑ 赵六 ☑ 钱七 ☑ 孙八│
+│ │ 全选 │ ☑ 周九 ☑ 吴十 ☑ 郑十一 ☑ 陈十二 ☑ 沈十三 │
+│ │ 取消全选 │ │
+│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
@@ -718,8 +833,11 @@ def _apply_manager_filter(self):
if not self.material_records_cache:
return
- # 获取选中的负责人列表
- selected_managers = self._get_selected_managers()
+ # PERMISSION CHECK: 非管理员用户强制筛选到当前用户
+ if not self.session_manager.is_admin():
+ selected_managers = [self.session_manager.get_username()]
+ else:
+ selected_managers = self._get_selected_managers()
if not selected_managers:
# 没有选中任何负责人,清空表格
@@ -735,12 +853,16 @@ def _apply_manager_filter(self):
]
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 表格编辑
-**功能**: 支持双击"负责人"单元格进行编辑。
+**功能**: 双击"负责人"单元格进行编辑。
**编辑流程**:
@@ -768,14 +890,13 @@ sequenceDiagram
```python
def _on_cell_double_click(self, event):
"""处理单元格双击事件,编辑负责人"""
- # 获取点击位置
region = self.tree.identify_region(event.x, event.y)
if region == "cell":
column = self.tree.identify_column(event.x)
item = self.tree.identify_row(event.y)
- # 检查是否点击了"负责人"列(第6列)
+ # 检查是否点击了"负责人"列(第 6 列)
if column == "#6" and item:
values = self.tree.item(item, "values")
current_value = values[5] if len(values) > 5 else ""
@@ -788,14 +909,84 @@ def _on_cell_double_click(self, event):
)
if new_value is not None: # 用户没有取消
- # 更新单元格值
new_values = list(values)
new_values[5] = new_value
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. 使用场景
@@ -807,7 +998,7 @@ def _on_cell_double_click(self, event):
**操作步骤**:
1. 选择"数据库 - 全表校验"模式
2. 点击"开始校验"
-3. 系统查询整个 `DiscreteMaterialPlanData` 表
+3. 系统查询整个 `DiscreteMaterialPlanData` 表(自动去重)
4. 使用负责人筛选器筛选特定负责人
5. 双击编辑负责人
6. 勾选需要删除的记录
@@ -820,11 +1011,12 @@ def _on_cell_double_click(self, event):
**操作步骤**:
1. 在"数据提取"页面输入 ProductionID
2. 切换到"物料校验"标签页
-3. 系统自动使用共享的 ProductionID
-4. 点击"开始校验"
+3. 系统自动使用共享的 Production ID(静默更新,无提示)
+4. 点击"开始校验"(自动使用 ProductionID 过滤模式)
5. 仅显示当前用户负责的物料
-6. 编辑负责人或勾选删除
-7. 点击"确认删除"保存
+6. 可使用"隐藏勾选"功能隐藏已处理记录
+7. 编辑负责人或勾选删除
+8. 点击"确认删除"保存
### 9.3 场景三:批量删除确认
@@ -840,15 +1032,15 @@ flowchart TD
AutoSync --> Confirm[点击确认删除按钮]
Confirm --> Validate{验证负责人}
- Validate -->|有缺失| ShowWarning[显示警告并列出缺少负责人的记录]
+ Validate -->|有缺失 | ShowWarning[显示警告并列出缺少负责人的记录]
ShowWarning --> Edit[用户编辑缺失的负责人]
Edit --> Confirm
- Validate -->|全部完整| ShowConfirm[显示确认对话框]
- ShowConfirm --> UserConfirm{用户确认?}
+ Validate -->|全部完整 | ShowConfirm[显示确认对话框]
+ ShowConfirm --> UserConfirm{用户确认?}
- UserConfirm -->|否| Cancel[取消操作]
- UserConfirm -->|是| Execute[执行后台同步]
+ UserConfirm -->|否 | Cancel[取消操作]
+ UserConfirm -->|是 | Execute[执行后台同步]
Execute --> Upsert[执行 upsert_batch 写入/更新勾选记录]
Execute --> Delete[执行 delete_by_material_codes 删除未勾选记录]
@@ -867,6 +1059,96 @@ flowchart TD
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. 技术要点
@@ -895,7 +1177,6 @@ def _update_log(self, message: str, level: str = "INFO"):
self.log_text.info(message)
elif level == "ERROR":
self.log_text.error(message)
- # ...
self.after(0, update) # 在主线程中执行
```
@@ -938,13 +1219,42 @@ def _batch_insert(self, db, df: pd.DataFrame, batch_size: int = 72) -> int:
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. 常见问题
### Q1: 为什么同一个 MaterialCode 会有多条记录?
-**A**: 因为同一个物料代码可能出现在不同的备料计划单号或生产订单号中。复选框同步机制确保了相同 MaterialCode 的记录会被一起选中/取消。
+**A**: 因为同一个物料代码可能出现在不同的备料计划单号或生产订单号中。系统现已实现 MaterialCode 去重机制:
+- 全表校验时自动去重
+- ProductionID 过滤时自动去重
+- 复选框同步机制确保相同 MaterialCode 的记录会被一起选中/取消
### Q2: 已标记删除(is_marked=true)和匹配到关键词有什么区别?
@@ -963,8 +1273,24 @@ def _batch_insert(self, db, df: pd.DataFrame, batch_size: int = 72) -> int:
### Q5: 如何理解"双优先级匹配"?
**A**:
-1. **优先级1(精确匹配)**: 如果 `MaterialCode` 在 `MaterialsToBeDeleted` 表中存在,使用该表的 `ManagerName`,并标记为 `is_marked=true`
-2. **优先级2(模糊匹配)**: 如果优先级1未匹配,检查 `MaterialName` 是否包含 `MaterialsTypeToBeDeleted` 表中的任何 `MaterialName`,如果包含,使用该表的 `ManagerName`
+1. **优先级 1(精确匹配)**: 如果 `MaterialCode` 在 `MaterialsToBeDeleted` 表中存在,使用该表的 `ManagerName`,并标记为 `is_marked=true`
+2. **优先级 2(模糊匹配)**: 如果优先级 1 未匹配,检查 `MaterialName` 是否包含 `MaterialsTypeToBeDeleted` 表中的任何 `MaterialName`,如果包含,使用该表的 `ManagerName`
+
+### Q6: User 模式的"隐藏勾选"功能如何使用?
+
+**A**:
+1. 勾选已处理完成的物料记录
+2. 点击右侧按钮面板的"隐藏勾选"按钮
+3. 已勾选的记录会从表格中隐藏(数据仍保留)
+4. 点击"显示全部"可恢复显示所有记录
+
+### Q7: 预览模式 (dryrun) 如何工作?
+
+**A**:
+- **Admin 用户**: 通过界面复选框控制
+- **User 用户**: 从配置文件 `execution.dryrun` 读取
+- 预览模式下,删除操作不会实际保存更改
+- 用于测试和验证删除逻辑
---
@@ -973,19 +1299,30 @@ def _batch_insert(self, db, df: pd.DataFrame, batch_size: int = 72) -> int:
### 12.1 性能优化
- 对于大数据量的全表查询,考虑添加分页功能
- 实现查询结果缓存,减少重复查询
+- 优化 MaterialCode 去重算法性能
### 12.2 功能增强
- 添加批量导入功能,支持从 Excel 导入负责人信息
- 实现导出模板功能,方便离线编辑
- 添加校验历史记录,追溯修改历史
+- 支持按规格、型号筛选物料
### 12.3 用户体验
- 实现拖拽排序功能
- 添加列筛选和排序功能
- 支持自定义列显示/隐藏
+- 增加搜索功能,快速定位物料
---
-**文档版本**: 1.0
-**最后更新**: 2026-02-24
+**文档版本**: 2.0
+**最后更新**: 2026-02-28
**维护者**: Development Team
+**更新说明**:
+- 新增 User 模式专属 UI 布局说明
+- 新增 MaterialCode 去重逻辑说明
+- 新增隐藏勾选物料功能说明
+- 更新权限控制说明(共享 Production ID、dryrun 配置)
+- 更新负责人筛选逻辑(双表获取、去重)
+- 更新删除确认流程(upsert/delete 分离)
+- 更新数据库表说明(upsert、get_all_records 等方法)