Files
playwrite/docs/MATERIAL_VALIDATION_INTERFACE.md
Misaka_Company a5672def19 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
2026-02-28 09:03:29 +08:00

43 KiB
Raw Permalink Blame History

物料校验界面实现说明文档

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 ASCSequenceNumber 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} 条隐藏的物料")

使用场景:

  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.1 场景一:管理员全表校验

需求: 管理员需要查看所有物料记录并进行校验

操作步骤:

  1. 选择"数据库 - 全表校验"模式
  2. 点击"开始校验"
  3. 系统查询整个 DiscreteMaterialPlanData 表(自动去重)
  4. 使用负责人筛选器筛选特定负责人
  5. 双击编辑负责人
  6. 勾选需要删除的记录
  7. 点击"确认删除"保存到数据库

9.2 场景二:普通用户按 ProductionID 校验

需求: 普通用户需要校验特定生产订单的物料

操作步骤:

  1. 在"数据提取"页面输入 ProductionID
  2. 切换到"物料校验"标签页
  3. 系统自动使用共享的 Production ID静默更新无提示
  4. 点击"开始校验"(自动使用 ProductionID 过滤模式)
  5. 仅显示当前用户负责的物料
  6. 可使用"隐藏勾选"功能隐藏已处理记录
  7. 编辑负责人或勾选删除
  8. 点击"确认删除"保存

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. 优先级 1精确匹配: 如果 MaterialCodeMaterialsToBeDeleted 表中存在,使用该表的 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 读取
  • 预览模式下,删除操作不会实际保存更改
  • 用于测试和验证删除逻辑

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 等方法)