feat: add database-driven material validation with multi-mode support

This commit enhances the material validation functionality to support
database-driven workflows alongside the existing Excel-based approach.

## New Features

### DAO Layer
- Add ProductionContractDataDAO for querying production contract data
- Add MaterialsToBeDeletedDAO with full CRUD operations
- Enhance DiscreteMaterialPlanDAO with query_all(), query_by_source_numbers(),
  and get_unique_material_names() methods

### Validation Modes
Support for 4 validation modes in MaterialValidationTab:
1. Database Full - Query all materials from DiscreteMaterialPlanData
2. Database Filtered - Query by ProductionID.txt file
3. Excel Existing - Validate from existing Excel file
4. Excel Full - Complete workflow with ERP extraction

### Configuration
- Add ValidationConfig dataclass with data_source, batch_size, match_mode,
  enable_crud_operations, and default_manager fields
- Update ConfigLoader to support validation configuration
- Add validation settings section in SettingsTab GUI

### Query Chain
Implementation of full query chain:
ProductionID.txt (总排号) → productionContractData (生产订单号) →
DiscreteMaterialPlanData (SourceNumber) → MaterialName →
MaterialsToBeDeleted comparison

## Backward Compatibility
All existing Excel-based validation methods remain unchanged, ensuring
no breaking changes for existing workflows.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-06 13:08:31 +08:00
parent 53a1e33e45
commit 2096a51f55
9 changed files with 976 additions and 57 deletions

View File

@@ -4,12 +4,17 @@
物料校验标签页
校验物料状态并匹配待删除物料。
支持 4 种校验模式:
1. database_full - 数据库全表校验
2. database_filtered - 数据库 ProductionID 过滤校验
3. excel_existing - Excel 现有文件校验
4. excel_full - Excel 完整工作流
"""
import os
import threading
import tkinter as tk
from tkinter import ttk, messagebox
from tkinter import ttk, messagebox, filedialog
from pathlib import Path
from io import StringIO
from contextlib import redirect_stdout
@@ -64,58 +69,95 @@ class MaterialValidationTab(ttk.Frame):
self._create_result_table(result_frame)
self._create_log_panel(log_frame)
# 初始化文件选择器显示状态
self._on_source_mode_change()
def _create_control_panel(self, parent):
"""创建控制面板"""
# 数据来源选择
source_group = ttk.LabelFrame(parent, text="数据来源", padding=10)
source_group.pack(fill=tk.X, pady=5)
self.source_mode = tk.StringVar(value="existing")
self.source_mode = tk.StringVar(value="database_full")
# 数据库模式
ttk.Radiobutton(
source_group,
text="使用现有 Excel 文件",
text="数据库 - 全表校验",
variable=self.source_mode,
value="existing",
value="database_full",
command=self._on_source_mode_change,
).grid(row=0, column=0, sticky="w", padx=5)
ttk.Radiobutton(
source_group,
text="完整工作流 (提取 + 校验)",
text="数据库 - ProductionID 过滤",
variable=self.source_mode,
value="full",
value="database_filtered",
command=self._on_source_mode_change,
).grid(row=0, column=1, sticky="w", padx=5)
# Excel 模式
ttk.Radiobutton(
source_group,
text="Excel - 现有文件",
variable=self.source_mode,
value="excel_existing",
command=self._on_source_mode_change,
).grid(row=1, column=0, sticky="w", padx=5)
ttk.Radiobutton(
source_group,
text="Excel - 完整工作流",
variable=self.source_mode,
value="excel_full",
command=self._on_source_mode_change,
).grid(row=1, column=1, sticky="w", padx=5)
# 文件选择
file_group = ttk.LabelFrame(parent, text="文件选择", padding=10)
file_group.pack(fill=tk.X, pady=5)
# 现有 Excel 文件
self.existing_excel_frame = ttk.Frame(file_group)
self.existing_excel_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
# 数据库全表模式 - 无需输入文件
self.db_full_frame = ttk.Frame(file_group)
ttk.Label(
self.db_full_frame,
text="数据库全表模式:将查询 DiscreteMaterialPlanData 表中的所有材料",
foreground="gray"
).pack(anchor="w")
self.existing_excel_selector = FileSelector(
self.existing_excel_frame,
label_text="现有 Excel 文件:",
file_type="file",
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
initial_dir=self.config.get("paths.data_dir", "data/"),
)
self.existing_excel_selector.pack(fill=tk.X)
# ProductionID 文件 (完整工作流模式)
self.production_id_frame = ttk.Frame(file_group)
# 初始隐藏
self.production_id_selector = FileSelector(
self.production_id_frame,
# 数据库过滤模式 - 需要 ProductionID 文件
self.db_filtered_frame = ttk.Frame(file_group)
self.db_filtered_production_id_selector = FileSelector(
self.db_filtered_frame,
label_text="ProductionID 文件:",
file_type="file",
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
initial_dir="D:/python/playwrite/",
)
self.production_id_selector.pack(fill=tk.X)
self.db_filtered_production_id_selector.pack(fill=tk.X)
# Excel 现有文件模式
self.excel_existing_frame = ttk.Frame(file_group)
self.excel_existing_selector = FileSelector(
self.excel_existing_frame,
label_text="现有 Excel 文件:",
file_type="file",
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
initial_dir=self.config.get("paths.data_dir", "data/"),
)
self.excel_existing_selector.pack(fill=tk.X)
# Excel 完整工作流模式
self.excel_full_frame = ttk.Frame(file_group)
self.excel_full_production_id_selector = FileSelector(
self.excel_full_frame,
label_text="ProductionID 文件:",
file_type="file",
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
initial_dir="D:/python/playwrite/",
)
self.excel_full_production_id_selector.pack(fill=tk.X)
# 输出文件
output_frame = ttk.Frame(file_group)
@@ -196,12 +238,23 @@ class MaterialValidationTab(ttk.Frame):
def _on_source_mode_change(self):
"""数据源模式切换"""
if self.source_mode.get() == "existing":
self.existing_excel_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
self.production_id_frame.grid_remove()
else:
self.existing_excel_frame.grid_remove()
self.production_id_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
mode = self.source_mode.get()
# 隐藏所有文件选择框架
self.db_full_frame.grid_remove()
self.db_filtered_frame.grid_remove()
self.excel_existing_frame.grid_remove()
self.excel_full_frame.grid_remove()
# 根据模式显示对应的文件选择器
if mode == "database_full":
self.db_full_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
elif mode == "database_filtered":
self.db_filtered_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
elif mode == "excel_existing":
self.excel_existing_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
elif mode == "excel_full":
self.excel_full_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
def start_validation(self):
"""开始校验"""
@@ -211,25 +264,38 @@ class MaterialValidationTab(ttk.Frame):
messagebox.showerror("错误", "请指定输出文件路径")
return
# 获取输入文件
if self.source_mode.get() == "existing":
input_file = self.existing_excel_selector.get()
if not input_file:
messagebox.showerror("错误", "请选择现有 Excel 文件")
return
if not os.path.exists(input_file):
messagebox.showerror("错误", f"文件不存在:{input_file}")
return
production_id_file = None
else:
production_id_file = self.production_id_selector.get()
mode = self.source_mode.get()
input_file = None
production_id_file = None
# 根据模式验证输入文件
if mode == "database_full":
# 无需输入文件
pass
elif mode == "database_filtered":
production_id_file = self.db_filtered_production_id_selector.get()
if not production_id_file:
messagebox.showerror("错误", "请选择 ProductionID 文件")
return
if not os.path.exists(production_id_file):
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
return
elif mode == "excel_existing":
input_file = self.excel_existing_selector.get()
if not input_file:
messagebox.showerror("错误", "请选择现有 Excel 文件")
return
if not os.path.exists(input_file):
messagebox.showerror("错误", f"文件不存在:{input_file}")
return
elif mode == "excel_full":
production_id_file = self.excel_full_production_id_selector.get()
if not production_id_file:
messagebox.showerror("错误", "请选择 ProductionID 文件")
return
if not os.path.exists(production_id_file):
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
return
input_file = None
# 确保输出目录存在
output_dir = os.path.dirname(output_file)
@@ -240,7 +306,7 @@ class MaterialValidationTab(ttk.Frame):
self.validating = True
self.start_button.config(state=tk.DISABLED)
self.log_text.clear()
self.log_text.info("开始物料校验...")
self.log_text.info(f"开始物料校验(模式: {mode}...")
# 清空结果表格
for item in self.tree.get_children():
@@ -249,13 +315,13 @@ class MaterialValidationTab(ttk.Frame):
# 在后台线程中执行校验
validation_thread = threading.Thread(
target=self._validation_worker,
args=(input_file, production_id_file, output_file),
args=(mode, input_file, production_id_file, output_file),
daemon=True,
)
validation_thread.start()
def _validation_worker(
self, input_file: str, production_id_file: str, output_file: str
self, mode: str, input_file: str, production_id_file: str, output_file: str
):
"""校验工作线程"""
try:
@@ -273,18 +339,32 @@ class MaterialValidationTab(ttk.Frame):
# 捕获 stdout 输出
captured_output = StringIO()
# 执行校验并捕获输出
# 根据模式执行校验
with redirect_stdout(captured_output):
if self.source_mode.get() == "existing":
result = validator.validate_from_existing_excel(
excel_file=input_file, output_file=output_file
if mode == "database_full":
result = validator.validate_from_database(
full_table=True,
output_file=output_file
)
else:
elif mode == "database_filtered":
result = validator.validate_from_database(
production_id_file=production_id_file,
full_table=False,
output_file=output_file
)
elif mode == "excel_existing":
result = validator.validate_from_existing_excel(
excel_file=input_file,
output_file=output_file
)
elif mode == "excel_full":
result = validator.validate(
production_id_file=production_id_file,
merged_excel_file=None, # 将在内部生成
merged_excel_file=None,
output_file=output_file,
)
else:
raise ValueError(f"未知的校验模式: {mode}")
# 获取捕获的输出并显示到日志
output_text = captured_output.getvalue()