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:
@@ -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()
|
||||
|
||||
@@ -57,9 +57,12 @@ class SettingsTab(ttk.Frame):
|
||||
# 处理配置组
|
||||
self._create_extraction_group(scrollable_frame)
|
||||
|
||||
# 校验配置组
|
||||
self._create_validation_group(scrollable_frame)
|
||||
|
||||
# 按钮区域
|
||||
button_frame = ttk.Frame(scrollable_frame)
|
||||
button_frame.grid(row=5, column=0, columnspan=2, pady=20, sticky="ew")
|
||||
button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew")
|
||||
|
||||
ttk.Button(
|
||||
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
|
||||
@@ -224,6 +227,63 @@ class SettingsTab(ttk.Frame):
|
||||
group, text="保存到数据库 (同时写入 SQL Server)", variable=self.enable_db_persistence_var
|
||||
).grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
|
||||
|
||||
def _create_validation_group(self, parent):
|
||||
"""创建物料校验配置组"""
|
||||
group = ttk.LabelFrame(parent, text="物料校验设置", padding=10)
|
||||
group.grid(row=4, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
|
||||
|
||||
# 数据源选择
|
||||
ttk.Label(group, text="默认数据源:").grid(row=0, column=0, sticky="w", pady=5)
|
||||
self.validation_data_source_var = tk.StringVar()
|
||||
data_source_combo = ttk.Combobox(
|
||||
group,
|
||||
textvariable=self.validation_data_source_var,
|
||||
values=["database_full", "database_filtered", "excel_existing", "excel_full"],
|
||||
state="readonly",
|
||||
width=30,
|
||||
)
|
||||
data_source_combo.grid(row=0, column=1, sticky="w", pady=5)
|
||||
|
||||
# 使用数据库
|
||||
self.validation_use_database_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(
|
||||
group, text="使用数据库作为数据源", variable=self.validation_use_database_var
|
||||
).grid(row=1, column=0, columnspan=2, sticky="w", pady=5)
|
||||
|
||||
# 批次大小
|
||||
ttk.Label(group, text="数据库批次大小:").grid(row=2, column=0, sticky="w", pady=5)
|
||||
self.validation_batch_size_var = tk.IntVar(value=2000)
|
||||
ttk.Spinbox(
|
||||
group, from_=100, to=2000, textvariable=self.validation_batch_size_var, width=10
|
||||
).grid(row=2, column=1, sticky="w", pady=5)
|
||||
|
||||
# 匹配模式
|
||||
ttk.Label(group, text="匹配模式:").grid(row=3, column=0, sticky="w", pady=5)
|
||||
self.validation_match_mode_var = tk.StringVar()
|
||||
match_mode_combo = ttk.Combobox(
|
||||
group,
|
||||
textvariable=self.validation_match_mode_var,
|
||||
values=["substring", "exact"],
|
||||
state="readonly",
|
||||
width=30,
|
||||
)
|
||||
match_mode_combo.grid(row=3, column=1, sticky="w", pady=5)
|
||||
|
||||
# CRUD 操作
|
||||
self.validation_enable_crud_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(
|
||||
group, text="启用 CRUD 操作(管理待删除物料)", variable=self.validation_enable_crud_var
|
||||
).grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
|
||||
|
||||
# 默认负责人
|
||||
ttk.Label(group, text="默认负责人:").grid(row=5, column=0, sticky="w", pady=5)
|
||||
self.validation_default_manager_var = tk.StringVar()
|
||||
ttk.Entry(group, textvariable=self.validation_default_manager_var, width=30).grid(
|
||||
row=5, column=1, sticky="w", pady=5
|
||||
)
|
||||
|
||||
group.columnconfigure(1, weight=1)
|
||||
|
||||
def load_settings(self):
|
||||
"""从配置加载设置到界面"""
|
||||
# ERP 设置
|
||||
@@ -255,6 +315,14 @@ class SettingsTab(ttk.Frame):
|
||||
self.merge_batches_var.set(self.config.get("extraction.merge_batches", True))
|
||||
self.enable_db_persistence_var.set(self.config.get("extraction.enable_db_persistence", False))
|
||||
|
||||
# 校验设置
|
||||
self.validation_data_source_var.set(self.config.get("validation.data_source", "database_full"))
|
||||
self.validation_use_database_var.set(self.config.get("validation.use_database", True))
|
||||
self.validation_batch_size_var.set(self.config.get("validation.batch_size", 2000))
|
||||
self.validation_match_mode_var.set(self.config.get("validation.match_mode", "substring"))
|
||||
self.validation_enable_crud_var.set(self.config.get("validation.enable_crud_operations", False))
|
||||
self.validation_default_manager_var.set(self.config.get("validation.default_manager", ""))
|
||||
|
||||
def save_settings(self):
|
||||
"""保存界面设置到配置"""
|
||||
# ERP 设置
|
||||
@@ -284,6 +352,14 @@ class SettingsTab(ttk.Frame):
|
||||
self.config.set("extraction.merge_batches", self.merge_batches_var.get())
|
||||
self.config.set("extraction.enable_db_persistence", self.enable_db_persistence_var.get())
|
||||
|
||||
# 校验设置
|
||||
self.config.set("validation.data_source", self.validation_data_source_var.get())
|
||||
self.config.set("validation.use_database", self.validation_use_database_var.get())
|
||||
self.config.set("validation.batch_size", self.validation_batch_size_var.get())
|
||||
self.config.set("validation.match_mode", self.validation_match_mode_var.get())
|
||||
self.config.set("validation.enable_crud_operations", self.validation_enable_crud_var.get())
|
||||
self.config.set("validation.default_manager", self.validation_default_manager_var.get())
|
||||
|
||||
# 保存到文件
|
||||
if self.config.save():
|
||||
messagebox.showinfo("成功", "设置已保存")
|
||||
|
||||
Reference in New Issue
Block a user