- 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
43 KiB
物料校验界面实现说明文档
1. 概述
功能简介
物料校验界面是一个用于校验物料状态并管理待删除物料的核心功能模块。该界面支持两种校验模式,实现了物料状态与数据库记录的智能匹配,并提供可视化的管理界面。
核心文件路径
gui/material_validation_tab.py - 主界面实现
utils/material_status_validator.py - 校验器核心逻辑
db/discrete_material_plan_dao.py - 物料数据访问
db/materials_to_be_deleted_dao.py - 待删除类型数据访问
db/materials_to_be_deleted_records_dao.py - 已标记记录数据访问
db/production_contract_data_dao.py - 生产合同数据访问
auth/session_manager.py - 会话管理(权限控制)
相关数据库表
- DiscreteMaterialPlanData - 离散备料计划数据表(主数据源)
- MaterialsTypeToBeDeleted - 待删除物料类型表(按物料名称匹配)
- MaterialsToBeDeleted - 已标记删除的物料记录表(按物料代码精确匹配)
- [productionContractData].[26 年压力表合同数据] - 生产合同数据表
2. 系统架构
2.1 整体架构图
graph TD
A[用户界面层] --> B[MaterialValidationTab]
B --> C[CheckboxTreeview]
B --> D[控制面板]
B --> E[负责人筛选器]
B --> F[日志面板]
D --> G[校验模式选择]
G --> H[database_full<br/>全表校验]
G --> I[database_filtered<br/>ProductionID 过滤]
H --> J[MaterialStatusValidator]
I --> J
J --> K[DiscreteMaterialPlanDAO]
J --> L[ProductionContractDataDAO]
J --> M[MaterialsTypeToBeDeletedDAO]
J --> N[MaterialsToBeDeletedDAO]
K --> O[(DiscreteMaterialPlanData)]
L --> P[(26 年压力表合同数据)]
M --> Q[(MaterialsTypeToBeDeleted)]
N --> R[(MaterialsToBeDeleted)]
B --> S[SessionManager]
S --> T{用户类型}
T -->|Admin| U[完整权限]
T -->|User| V[受限权限]
style A fill:#e1f5ff
style B fill:#fff4e1
style J fill:#ffe1f5
style O fill:#e1ffe1
style P fill:#e1ffe1
style Q fill:#e1ffe1
style R fill:#e1ffe1
2.2 组件关系图
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 {
+set_checked(item, checked)
+get_checked_items()
+check_all(checked)
-_on_click(event)
}
class MaterialStatusValidator {
+validate_from_database_enhanced()
+match_materials_detailed()
-_get_source_numbers_from_production_ids()
-_get_material_names_from_db()
}
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_总排号 ()
}
class MaterialsTypeToBeDeletedDAO {
+get_all_materials()
+get_managers()
}
class MaterialsToBeDeletedDAO {
+get_all_records()
+upsert_material()
+upsert_batch()
+delete_by_material_codes()
+get_managers()
}
class SessionManager {
+is_admin()
+get_username()
+login()
}
class MaterialValidationResult {
+material_name
+material_code
+manager_name
+is_marked_for_deletion
+matched_type_keyword
}
MaterialValidationTab --> CheckboxTreeview : contains
MaterialValidationTab --> MaterialStatusValidator : uses
MaterialValidationTab --> SessionManager : uses
MaterialStatusValidator --> DiscreteMaterialPlanDAO : queries
MaterialStatusValidator --> ProductionContractDataDAO : queries
MaterialStatusValidator --> MaterialsTypeToBeDeletedDAO : queries
MaterialStatusValidator --> MaterialsToBeDeletedDAO : queries
MaterialStatusValidator --> MaterialValidationResult : creates
3. 数据流程
3.1 校验流程图
flowchart TD
Start([开始校验]) --> CheckMode{选择校验模式}
CheckMode -->|database_full| FullMode[全表校验]
CheckMode -->|database_filtered| FilterMode[过滤校验]
FullMode --> QueryAll[查询 DiscreteMaterialPlanData<br/>获取所有记录并去重]
FilterMode --> ReadProductionID[读取 ProductionID.txt<br/>或共享 Production ID]
ReadProductionID --> QueryContract[查询生产合同数据<br/>获取 SourceNumber]
QueryContract --> QueryBySource[按 SourceNumber<br/>查询物料记录并去重]
QueryAll --> FetchType[获取待删除物料类型]
QueryBySource --> FetchType
FetchType --> GetTypeRecords[MaterialsTypeToBeDeleted<br/>获取所有记录]
GetTypeRecords --> GetMarkedRecords[MaterialsToBeDeleted<br/>获取已标记记录]
GetMarkedRecords --> BuildDict[构建 MaterialCode->ManagerName<br/>映射字典]
BuildDict --> Match[执行双优先级匹配]
Match --> Priority1{优先级 1:<br/>精确匹配?}
Priority1 -->|是 | SetMarked[设置 is_marked=true<br/>使用 MaterialsToBeDeleted.ManagerName]
Priority1 -->|否 | Priority2{优先级 2:<br/>模糊匹配?}
Priority2 -->|是 | SetKeyword[设置 matched_keyword<br/>使用 MaterialsTypeToBeDeleted.ManagerName]
Priority2 -->|否 | SetUnmatched[设置 manager_name=null<br/>is_marked=false]
SetMarked --> CreateResult[创建 MaterialValidationResult]
SetKeyword --> CreateResult
SetUnmatched --> CreateResult
CreateResult --> Cache[缓存结果记录]
Cache --> Display[显示在界面表格]
Display --> InitFilter[初始化负责人筛选器]
InitFilter --> End([完成])
style Start fill:#e1f5ff
style End fill:#e1f5ff
style Match fill:#ffe1f5
style CreateResult fill:#fff4e1
3.2 数据查询链路图
flowchart LR
A[ProductionID.txt<br/>总排号列表] --> B[ProductionContractDataDAO]
B --> C[[26 年压力表合同数据]]
C --> D[SourceNumber<br/>生产订单号]
D --> E[DiscreteMaterialPlanDAO]
E --> F[[DiscreteMaterialPlanData]]
F --> G[物料记录列表<br/>MaterialCode, MaterialName<br/>Specification, Model]
G --> H[MaterialStatusValidator]
H --> I[MaterialsTypeToBeDeletedDAO]
H --> J[MaterialsToBeDeletedDAO]
I --> K[[MaterialsTypeToBeDeleted]]
J --> L[[MaterialsToBeDeleted]]
K --> M[物料类型匹配<br/>MaterialName 包含匹配]
L --> N[精确匹配<br/>MaterialCode 精确匹配]
M --> O[双优先级匹配算法]
N --> O
O --> P[MaterialValidationResult<br/>校验结果]
style A fill:#ffe1f5
style F fill:#ffe1f5
style K fill:#ffe1f5
style L fill:#ffe1f5
style P fill:#e1f5ff
3.3 MaterialCode 去重逻辑
新增功能: 为避免同一物料代码出现多条记录,系统实现了基于 MaterialCode 的去重机制。
去重策略:
- 排序规则:
CreateDate ASC→SequenceNumber ASC - 保留规则: 每个 MaterialCode 保留第一条记录
- 日志输出: 显示移除的重复记录数量
实现方法:
# 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 交互时序图
sequenceDiagram
actor User as 用户
participant UI as MaterialValidationTab
participant Validator as MaterialStatusValidator
participant DAO1 as DiscreteMaterialPlanDAO
participant DAO2 as ProductionContractDataDAO
participant DAO3 as MaterialsTypeToBeDeletedDAO
participant DAO4 as MaterialsToBeDeletedDAO
User->>UI: 点击"开始校验"
UI->>UI: 验证输入文件
UI->>Validator: 创建校验器实例
UI->>UI: 启动后台线程
alt 全表校验模式
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_distinct()
DAO1-->>Validator: 返回去重后的记录
end
Validator->>DAO3: get_all_materials()
DAO3-->>Validator: 返回物料类型记录
Validator->>DAO4: get_all_records()
DAO4-->>Validator: 返回已标记记录
Validator->>Validator: match_materials_detailed()
Note over Validator: 执行双优先级匹配
Validator-->>UI: 返回校验结果
UI->>UI: 缓存结果记录
UI->>UI: 更新表格显示
UI->>UI: 初始化负责人筛选器
UI-->>User: 显示校验完成
4. 核心组件说明
4.1 MaterialValidationTab 类
职责: 物料校验标签页的主界面类
核心方法:
| 方法名 | 功能说明 |
|---|---|
start_validation() |
启动校验流程,根据选择的模式调用相应的校验方法 |
_validation_worker_enhanced() |
后台工作线程,执行增强的数据库校验 |
confirm_deletion() |
确认删除操作主流程,处理勾选状态 |
_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 过滤模式,只能看到自己的数据,自动筛选到当前用户,使用两列布局(表格 + 右侧按钮面板)
4.2 CheckboxTreeview 类
职责: 支持复选框功能的 Treeview 组件
核心特性:
- 使用 Unicode 字符
☐和☑模拟 checkbox - 支持单个点击切换状态
- 支持全选/取消全选操作
- 支持状态变化回调
核心方法:
| 方法名 | 功能说明 |
|---|---|
set_checked(item, checked) |
设置指定 item 的 checkbox 状态 |
get_checked_items() |
获取所有选中的 item |
check_all(checked) |
全选或取消全选 |
_on_click(event) |
处理点击事件,切换 checkbox 状态 |
4.3 MaterialStatusValidator 类
职责: 物料状态校验器,负责数据查询和匹配逻辑
核心方法:
| 方法名 | 功能说明 |
|---|---|
validate_from_database_enhanced() |
增强的数据库校验(完整记录模式,支持去重) |
match_materials_detailed() |
匹配物料并返回详细结果 |
_read_production_ids() |
读取 ProductionID.txt 文件 |
_get_source_numbers_from_inputs() |
通过总排号查询获取生产订单号 |
_get_material_names_from_db() |
从数据库获取材料名称 |
数据结构:
@dataclass
class MaterialValidationResult:
material_name: str # 材料名称
material_code: str # 材料代码
specification: Optional[str] # 规格
model: Optional[str] # 型号
manager_name: Optional[str] # 负责人
is_marked_for_deletion: bool # 是否已标记删除
matched_type_keyword: Optional[str] # 匹配的关键词
5. 数据库表结构
5.1 DiscreteMaterialPlanData(离散备料计划数据表)
用途: 存储离散备料计划的主数据,是校验的主要数据源
关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| PlanNumber | varchar | 备料计划单号 |
| SourceNumber | varchar | 来源单号(生产订单号) |
| MaterialCode | varchar | 材料编码(用于精确匹配) |
| MaterialName | varchar | 材料名称 |
| Specification | varchar | 规格 |
| Model | varchar | 型号 |
| ManagerName | varchar | 负责人 |
查询方法:
# 查询所有记录(去重)
dao = DiscreteMaterialPlanDAO()
records = dao.query_all_distinct_by_material_code()
# 按生产订单号查询(去重)
records = dao.query_by_source_numbers_distinct(source_numbers)
# 获取唯一材料名称
material_names = dao.get_unique_material_names(source_numbers)
5.2 MaterialsTypeToBeDeleted(待删除物料类型表)
用途: 存储按物料名称匹配的待删除物料(模糊匹配)
关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| MaterialName | varchar | 物料名称(用于包含匹配) |
| ManagerName | varchar | 负责人 |
匹配规则: 如果 DiscreteMaterialPlanData.MaterialName 包含 MaterialsTypeToBeDeleted.MaterialName,则匹配成功
查询方法:
dao = MaterialsTypeToBeDeletedDAO()
# 获取所有物料类型
materials = dao.get_all_materials()
# 获取所有负责人
managers = dao.get_managers()
# 按负责人查询
materials = dao.get_materials_by_manager('张三')
5.3 MaterialsToBeDeleted(已标记删除的物料记录表)
用途: 存储已标记删除的具体物料记录(精确匹配)
关键字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| ID | int | 主键 |
| MaterialCode | varchar | 物料代码(用于精确匹配) |
| ManagerName | varchar | 负责人 |
匹配规则: 如果 DiscreteMaterialPlanData.MaterialCode 等于 MaterialsToBeDeleted.MaterialCode,则匹配成功
操作方法:
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')
# 批量删除
deleted_count = dao.delete_by_material_codes(['M001', 'M002', 'M003'])
MERGE 语句实现 (SQL Server):
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);
6. 匹配算法逻辑
6.1 双优先级匹配机制
物料校验采用双优先级匹配机制,确保精确匹配优先于模糊匹配:
flowchart TD
Start[物料记录] --> CheckP1{优先级 1:<br/>MaterialsToBeDeleted<br/>MaterialCode 精确匹配?}
CheckP1 -->|是 | Marked[已标记删除]
Marked --> SetM1[设置 ManagerName<br/>= MaterialsToBeDeleted.ManagerName]
SetM1 --> SetFlag1[is_marked_for_deletion = true]
SetFlag1 --> End1[返回结果]
CheckP1 -->|否 | CheckP2{优先级 2:<br/>MaterialsTypeToBeDeleted<br/>MaterialName 包含匹配?}
CheckP2 -->|是 | Keyword[匹配到关键词]
Keyword --> SetM2[设置 ManagerName<br/>= MaterialsTypeToBeDeleted.ManagerName]
SetM2 --> SetKeyword[matched_type_keyword<br/>= 匹配的 MaterialName]
SetKeyword --> SetFlag2[is_marked_for_deletion = false]
SetFlag2 --> End2[返回结果]
CheckP2 -->|否 | Unmatched[未匹配]
Unmatched --> SetNull[manager_name = null]
SetNull --> SetFlag3[is_marked_for_deletion = false<br/>matched_type_keyword = null]
SetFlag3 --> End3[返回结果]
style Start fill:#e1f5ff
style Marked fill:#ffe1f5
style Keyword fill:#fff4e1
style Unmatched fill:#f5f5f5
6.2 匹配代码实现
def match_materials_detailed(
self,
material_records: List[Dict[str, Any]],
type_keywords: List[Dict[str, Any]],
marked_codes_dict: Dict[str, str]
) -> List[MaterialValidationResult]:
"""
双优先级匹配算法
Args:
material_records: DiscreteMaterialPlanData 的完整记录(已去重)
type_keywords: MaterialsTypeToBeDeleted 记录(模糊匹配)
marked_codes_dict: MaterialCode->ManagerName 映射(精确匹配)
Returns:
List[MaterialValidationResult]: 匹配结果列表
"""
results = []
for record in material_records:
material_name = record.get('MaterialName', '') or ''
material_code = record.get('MaterialCode', '') or ''
specification = record.get('Specification', '') or None
model = record.get('Model', '') or None
# ===== 优先级 1: 精确匹配 =====
manager_name = marked_codes_dict.get(material_code) if material_code else None
is_marked = manager_name is not None
matched_keyword = None
# ===== 优先级 2: 模糊匹配 =====
if not manager_name:
for type_record in type_keywords:
type_material_name = type_record.get('MaterialName', '')
if type_material_name and type_material_name in material_name:
matched_keyword = type_material_name
manager_name = type_record.get('ManagerName')
break
result = MaterialValidationResult(
material_name=material_name,
material_code=material_code,
specification=specification,
model=model,
manager_name=manager_name,
is_marked_for_deletion=is_marked,
matched_type_keyword=matched_keyword
)
results.append(result)
return results
6.3 匹配示例
| MaterialCode | MaterialName | MaterialsToBeDeleted | MaterialsTypeToBeDeleted | 匹配结果 | is_marked | ManagerName |
|---|---|---|---|---|---|---|
| M001 | 螺栓 M8×20 | M001 → 张三 | - | 优先级 1 精确匹配 | true | 张三 |
| M002 | 垫圈 Φ8 | - | 垫圈 → 李四 | 优先级 2 模糊匹配 | false | 李四 |
| M003 | 螺母 M6 | - | - | 未匹配 | false | null |
| M004 | 不锈钢螺栓 M10×30 | M004 → 王五 | 螺栓 → 赵六 | 优先级 1 精确匹配(忽略螺栓) | true | 王五 |
7. 权限控制
7.1 用户类型
系统通过 SessionManager 实现基于角色的访问控制(RBAC):
classDiagram
class SessionManager {
<<Singleton>>
-_current_user: Dict
+login(username, password)
+is_admin() bool
+get_username() str
+get_user_type() str
}
class User {
<<Abstract>>
+username: str
+user_type: str
}
class Admin {
+user_type: 'Admin'
+can_select_data_source: true
+can_view_all_managers: true
+can_use_manager_filter: true
+can_configure_dryrun: true
}
class NormalUser {
+user_type: 'User'
+can_select_data_source: false
+can_view_all_managers: false
+can_use_manager_filter: false
+dryrun_from_config: true
}
SessionManager --> User : manages
User <|-- Admin
User <|-- NormalUser
7.2 Admin vs User 权限差异
| 功能 | Admin | User |
|---|---|---|
| 数据来源选择 | ✅ 可选择全表/过滤 | ❌ 仅限过滤模式 |
| ProductionID 数据源 | ✅ 可选择文件或共享 | ❌ 仅限共享 ID |
| 查看数据范围 | ✅ 所有负责人的数据 | ❌ 仅自己的数据 |
| 负责人筛选 | ✅ 可使用复选框筛选 | ❌ 自动筛选到当前用户 |
| 编辑负责人 | ✅ 可编辑任何人 | ✅ 可编辑任何人 |
| 删除确认 | ✅ 可操作所有人 | ✅ 可操作所有人 |
| 预览模式 (dryrun) | ✅ 界面复选框控制 | ✅ 从配置 execution.dryrun 读取 |
| 界面布局 | 按钮在顶部控制面板 | 两列布局(表格 + 右侧按钮面板) |
| 专属按钮 | - | ✅ 隐藏勾选、显示全部按钮 |
7.3 共享 Production ID 管理
代码位置: gui/material_validation_tab.py:57-58, 519-556
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 权限检查实现
# 检查是否为管理员
if self.session_manager.is_admin():
# 显示管理员专属控件
self.source_mode = tk.StringVar(value="database_full")
# 显示负责人筛选区域
manager_filter_frame = ttk.LabelFrame(...)
else:
# 普通用户默认设置
self.source_mode = tk.StringVar(value="database_filtered")
# User 模式:两列布局
self._create_user_filter_buttons(main_result_container)
7.5 数据过滤逻辑
def _get_selected_managers(self) -> List[str]:
"""获取选中的负责人列表"""
# PERMISSION CHECK: 非管理员用户直接返回当前用户名
if not self.session_manager.is_admin():
return [self.session_manager.get_username()]
# 管理员:从复选框获取选中的负责人
return [
manager for manager, var in self.manager_checkboxes.items()
if var.get()
]
8. 交互功能
8.1 复选框同步机制
功能: 当用户点击某个记录的 checkbox 时,自动将所有具有相同材料代码的记录的 checkbox 状态同步更新。
实现逻辑:
flowchart TD
UserClick[用户点击 checkbox] --> GetItem[获取点击的 item]
GetItem --> GetCode[获取该行的 MaterialCode]
GetCode --> Iterate[遍历表格所有行]
Iterate --> CheckCode{MaterialCode<br/>相同?}
CheckCode -->|是 | CheckState{状态<br/>不同?}
CheckState -->|是 | Update[更新 checkbox 状态]
CheckState -->|否 | Next[继续下一行]
CheckCode -->|否 | Next
Update --> Next
Next --> MoreRows{还有行?}
MoreRows -->|是 | Iterate
MoreRows -->|否 | Log[记录同步数量]
Log --> End[完成]
style UserClick fill:#e1f5ff
style Update fill:#ffe1f5
style End fill:#e1ffe1
代码实现:
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)
# 同步所有具有相同材料代码的记录
synced_count = 0
for item in self.tree.get_children():
item_values = self.tree.item(item, "values")
if item_values and len(item_values) > 2:
if item_values[2] == material_code and item != changed_item:
# 只更新状态不同的行,避免重复更新
current_state = self.tree.checkboxes.get(item, False)
if current_state != new_state:
self.tree.set_checked(item, new_state)
synced_count += 1
if synced_count > 0:
action = "选中" if new_state else "取消选中"
self.log_text.info(f"已同步 {synced_count} 条相同材料代码的记录{action}")
8.2 负责人筛选
功能: 管理员可以通过复选框筛选显示特定负责人的物料记录。
负责人列表来源:
MaterialsToBeDeletedDAO.get_managers()- 已标记删除记录表MaterialsTypeToBeDeletedDAO.get_managers()- 物料类型表- 合并后去重排序
界面布局 (左右两列):
┌─────────────────────────────────────────────────────────────┐
│ 筛选(按负责人) │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ☑ 张三 ☑ 李四 ☑ 王五 ☑ 赵六 ☑ 钱七 ☑ 孙八│
│ │ 全选 │ ☑ 周九 ☑ 吴十 ☑ 郑十一 ☑ 陈十二 ☑ 沈十三 │
│ │ 取消全选 │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
筛选规则:
- 显示选中负责人的记录
- 同时显示负责人为空的记录(待编辑)
- 未选中任何负责人时,表格清空
代码实现:
def _apply_manager_filter(self):
"""应用负责人筛选"""
if not self.material_records_cache:
return
# 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:
# 没有选中任何负责人,清空表格
self._refresh_filtered_results([])
self._update_log("未选择任何负责人,表格已清空", "WARNING")
return
# 筛选记录:包含选中负责人的记录 + 负责人为空的记录
filtered_records = [
record for record in self.material_records_cache
if (record.manager_name in selected_managers or
not record.manager_name or record.manager_name.strip() == "")
]
self._refresh_filtered_results(filtered_records)
self._update_log(
f"筛选结果:共 {len(filtered_records)} 条记录"
f"(其中 {empty_manager_count} 条负责人为空,待编辑)",
"INFO"
)
8.3 表格编辑
功能: 双击"负责人"单元格进行编辑。
编辑流程:
sequenceDiagram
actor User
participant Tree as CheckboxTreeview
participant Dialog as SimpleDialog
participant Log as LogText
User->>Tree: 双击"负责人"单元格
Tree->>Tree: 识别点击位置和列
Tree->>Tree: 获取当前值
Tree->>Dialog: 弹出编辑对话框
Dialog-->>User: 显示输入框
User->>Dialog: 输入新值
Dialog-->>Tree: 返回新值
Tree->>Tree: 更新单元格显示
Tree->>Log: 记录更新日志
Log-->>User: 显示更新成功消息
代码实现:
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 列)
if column == "#6" and item:
values = self.tree.item(item, "values")
current_value = values[5] if len(values) > 5 else ""
# 弹出编辑对话框
new_value = simpledialog.askstring(
"编辑负责人",
f"请输入负责人姓名:",
initialvalue=current_value
)
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}")
8.4 隐藏勾选物料功能(User 模式专属)
功能: 隐藏已勾选的物料记录,方便用户专注处理未勾选的物料。
代码位置: gui/material_validation_tab.py:433-459
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} 条隐藏的物料")
使用场景:
- User 模式下,用户勾选了已处理完成的物料
- 点击"隐藏勾选"按钮,隐藏已处理记录
- 专注处理剩余的未勾选物料
- 完成后点击"显示全部"恢复显示
8.5 User 模式专属 UI 布局
代码位置: gui/material_validation_tab.py:260-431
布局对比:
| 模式 | 布局描述 |
|---|---|
| Admin | 表格占满整个区域,按钮在顶部控制面板 |
| User | 两列布局:左侧表格 + 右侧按钮面板 |
User 模式界面布局:
┌────────────────────────────────────────────────────────────┐
│ 校验结果 │
├───────────────────────────┬────────────────────────────────┤
│ 表格区域 │ 隐藏勾选 │
│ ┌─────────────────────┐ │ 显示全部 │
│ │ ☐ 物料 1 ... │ │ ──────────── │
│ │ ☑ 物料 2 ... │ │ 全选 │
│ │ ☐ 物料 3 ... │ │ 取消全选 │
│ │ ☐ 物料 4 ... │ │ 确认删除 │
│ └─────────────────────┘ │ 执行删除 │
└───────────────────────────┴────────────────────────────────┘
按钮可见性:
- 隐藏勾选、显示全部: 仅 User 模式可见
- 全选、取消全选、确认删除、执行删除: Admin 模式在顶部,User 模式在右侧
9. 使用场景
9.1 场景一:管理员全表校验
需求: 管理员需要查看所有物料记录并进行校验
操作步骤:
- 选择"数据库 - 全表校验"模式
- 点击"开始校验"
- 系统查询整个
DiscreteMaterialPlanData表(自动去重) - 使用负责人筛选器筛选特定负责人
- 双击编辑负责人
- 勾选需要删除的记录
- 点击"确认删除"保存到数据库
9.2 场景二:普通用户按 ProductionID 校验
需求: 普通用户需要校验特定生产订单的物料
操作步骤:
- 在"数据提取"页面输入 ProductionID
- 切换到"物料校验"标签页
- 系统自动使用共享的 Production ID(静默更新,无提示)
- 点击"开始校验"(自动使用 ProductionID 过滤模式)
- 仅显示当前用户负责的物料
- 可使用"隐藏勾选"功能隐藏已处理记录
- 编辑负责人或勾选删除
- 点击"确认删除"保存
9.3 场景三:批量删除确认
需求: 用户需要批量确认删除多个物料记录
操作流程:
flowchart TD
Start([开始]) --> Select[选择物料记录]
Select --> CheckAll[点击全选按钮]
CheckAll --> AutoSync[系统自动同步相同 MaterialCode 的记录]
AutoSync --> Confirm[点击确认删除按钮]
Confirm --> Validate{验证负责人}
Validate -->|有缺失 | ShowWarning[显示警告并列出缺少负责人的记录]
ShowWarning --> Edit[用户编辑缺失的负责人]
Edit --> Confirm
Validate -->|全部完整 | ShowConfirm[显示确认对话框]
ShowConfirm --> UserConfirm{用户确认?}
UserConfirm -->|否 | Cancel[取消操作]
UserConfirm -->|是 | Execute[执行后台同步]
Execute --> Upsert[执行 upsert_batch 写入/更新勾选记录]
Execute --> Delete[执行 delete_by_material_codes 删除未勾选记录]
Upsert --> Complete[显示完成消息]
Delete --> Complete
Complete --> Refresh[刷新筛选器和结果]
Refresh --> End([结束])
Cancel --> End
style Start fill:#e1f5ff
style End fill:#e1f5ff
style ShowWarning fill:#fff4e1
style Execute fill:#ffe1f5
style Complete fill:#e1ffe1
删除确认流程代码:
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 系统中的数据
操作流程:
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.1 线程安全
校验操作在后台线程中执行,避免阻塞 UI:
# 在后台线程中执行校验
validation_thread = threading.Thread(
target=self._validation_worker_enhanced,
args=(mode, input_file, production_id_file, output_file, production_ids_list),
daemon=True,
)
validation_thread.start()
日志更新通过 after() 方法确保线程安全:
def _update_log(self, message: str, level: str = "INFO"):
"""线程安全的日志更新"""
def update():
if level == "INFO":
self.log_text.info(message)
elif level == "ERROR":
self.log_text.error(message)
self.after(0, update) # 在主线程中执行
10.2 数据库兼容性
系统通过 _convert_sql() 和 _get_placeholder() 方法实现 SQL Server 和 MySQL 的兼容:
# SQL Server
SELECT [MaterialName], [ManagerName]
FROM [dbo].[MaterialsTypeToBeDeleted]
WHERE [MaterialName] = ?
# MySQL
SELECT MaterialName, ManagerName
FROM `MaterialsTypeToBeDeleted`
WHERE MaterialName = ?
10.3 批量操作优化
为避免 SQL Server 参数限制(2100 个),批量操作采用分批处理:
def _batch_insert(self, db, df: pd.DataFrame, batch_size: int = 72) -> int:
"""
Batch insert records (max 72 per batch due to SQL Server 2100 param limit).
With 29 fields, the maximum batch size is floor(2100 / 29) = 72 records.
"""
total_inserted = 0
records = self._convert_df_to_records(df)
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
for record in batch:
db.execute_update(sql, record)
total_inserted += 1
return total_inserted
10.4 临时文件管理
使用共享 Production ID 时,创建临时文件供校验器使用:
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 去重机制:
- 全表校验时自动去重
- ProductionID 过滤时自动去重
- 复选框同步机制确保相同 MaterialCode 的记录会被一起选中/取消
Q2: 已标记删除(is_marked=true)和匹配到关键词有什么区别?
A:
- 已标记删除: 来自
MaterialsToBeDeleted表,通过MaterialCode精确匹配,优先级最高 - 匹配到关键词: 来自
MaterialsTypeToBeDeleted表,通过MaterialName包含匹配,优先级次之
Q3: User 用户为什么看不到"数据来源"选项?
A: 这是权限控制的设计。User 用户只能使用 ProductionID 过滤模式,确保他们只能访问相关的数据,而不是整个数据库。
Q4: 编辑负责人后,为什么需要重新点击"确认删除"?
A: 编辑负责人只是修改了界面显示,并未保存到数据库。只有点击"确认删除"后,修改才会被写入数据库。
Q5: 如何理解"双优先级匹配"?
A:
- 优先级 1(精确匹配): 如果
MaterialCode在MaterialsToBeDeleted表中存在,使用该表的ManagerName,并标记为is_marked=true - 优先级 2(模糊匹配): 如果优先级 1 未匹配,检查
MaterialName是否包含MaterialsTypeToBeDeleted表中的任何MaterialName,如果包含,使用该表的ManagerName
Q6: User 模式的"隐藏勾选"功能如何使用?
A:
- 勾选已处理完成的物料记录
- 点击右侧按钮面板的"隐藏勾选"按钮
- 已勾选的记录会从表格中隐藏(数据仍保留)
- 点击"显示全部"可恢复显示所有记录
Q7: 预览模式 (dryrun) 如何工作?
A:
- Admin 用户: 通过界面复选框控制
- User 用户: 从配置文件
execution.dryrun读取 - 预览模式下,删除操作不会实际保存更改
- 用于测试和验证删除逻辑
12. 扩展建议
12.1 性能优化
- 对于大数据量的全表查询,考虑添加分页功能
- 实现查询结果缓存,减少重复查询
- 优化 MaterialCode 去重算法性能
12.2 功能增强
- 添加批量导入功能,支持从 Excel 导入负责人信息
- 实现导出模板功能,方便离线编辑
- 添加校验历史记录,追溯修改历史
- 支持按规格、型号筛选物料
12.3 用户体验
- 实现拖拽排序功能
- 添加列筛选和排序功能
- 支持自定义列显示/隐藏
- 增加搜索功能,快速定位物料
文档版本: 2.0 最后更新: 2026-02-28 维护者: Development Team 更新说明:
- 新增 User 模式专属 UI 布局说明
- 新增 MaterialCode 去重逻辑说明
- 新增隐藏勾选物料功能说明
- 更新权限控制说明(共享 Production ID、dryrun 配置)
- 更新负责人筛选逻辑(双表获取、去重)
- 更新删除确认流程(upsert/delete 分离)
- 更新数据库表说明(upsert、get_all_records 等方法)