feat: add Tkinter GUI application for ERP automation tools

Implement a comprehensive GUI application with the following features:
- Data Extraction tab: Extract material plan data from ERP system
- Material Validation tab: Validate material status and match deletions
- Data Query tab: Query database for production order information
- Settings tab: Manage ERP, database, browser, and path configurations

Key components:
- MainWindow: Tabbed interface with status bar
- ConfigManager: JSON-based configuration management
- LogText: Custom read-only text widget with colored logging
- FileSelector: Reusable file/directory selection component
- ProgressDialog: Modal progress dialog for long operations

Technical details:
- Thread-safe UI updates using root.after()
- Stdout capture for legacy script integration
- Event-based readonly mode allowing copy/select operations
- Custom widget composition to avoid Tkinter ScrolledText issues

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-05 13:33:23 +08:00
parent c9e68a2bf0
commit 71621dc8a0
14 changed files with 1977 additions and 1 deletions

View File

@@ -0,0 +1,374 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
物料校验标签页
校验物料状态并匹配待删除物料。
"""
import os
import threading
import tkinter as tk
from tkinter import ttk, messagebox
from pathlib import Path
from io import StringIO
from contextlib import redirect_stdout
from gui.widgets import FileSelector, LogText
from gui.config_manager import ConfigManager
import pandas as pd
class MaterialValidationTab(ttk.Frame):
"""物料校验标签页"""
def __init__(self, parent, config: ConfigManager):
"""
初始化物料校验标签页
Args:
parent: 父容器
config: 配置管理器
"""
super().__init__(parent)
self.config = config
self.validating = False
self.validation_results = None
self.create_widgets()
# 稍后显示就绪消息
try:
self.log_text.info("物料校验标签页已就绪")
except:
pass # 如果窗口还未完全就绪,忽略错误
def create_widgets(self):
"""创建界面组件"""
# 主容器
main_container = ttk.Frame(self)
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 上部:控制面板
control_frame = ttk.Frame(main_container)
control_frame.pack(fill=tk.X, pady=(0, 10))
# 中部:结果表格
result_frame = ttk.LabelFrame(main_container, text="校验结果", padding=5)
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 下部:日志输出
log_frame = ttk.LabelFrame(main_container, text="日志", padding=5)
log_frame.pack(fill=tk.BOTH, expand=True)
self._create_control_panel(control_frame)
self._create_result_table(result_frame)
self._create_log_panel(log_frame)
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")
ttk.Radiobutton(
source_group,
text="使用现有 Excel 文件",
variable=self.source_mode,
value="existing",
command=self._on_source_mode_change
).grid(row=0, column=0, sticky="w", padx=5)
ttk.Radiobutton(
source_group,
text="完整工作流 (提取 + 校验)",
variable=self.source_mode,
value="full",
command=self._on_source_mode_change
).grid(row=0, 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.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,
label_text="ProductionID 文件:",
file_type="file",
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
initial_dir="D:/python/playwrite/"
)
self.production_id_selector.pack(fill=tk.X)
# 输出文件
output_frame = ttk.Frame(file_group)
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10, 0))
self.output_file_selector = FileSelector(
output_frame,
label_text="输出文件:",
file_type="file",
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
initial_dir=self.config.get('paths.data_dir', 'data/')
)
self.output_file_selector.pack(fill=tk.X)
# 设置默认输出
default_output = os.path.join(
self.config.get('paths.data_dir', 'data/'),
self.config.get('paths.validation_output', '物料状态校验结果.xlsx')
)
self.output_file_selector.set(default_output)
# 控制按钮
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, pady=10)
self.start_button = ttk.Button(button_frame, text="开始校验", command=self.start_validation)
self.start_button.pack(side=tk.LEFT, padx=5)
self.export_button = ttk.Button(button_frame, text="导出结果", command=self.export_results, state=tk.DISABLED)
self.export_button.pack(side=tk.LEFT, padx=5)
def _create_result_table(self, parent):
"""创建结果表格"""
# 创建 Treeview
columns = ("材料名称", "匹配的MaterialName", "负责人", "匹配状态")
self.tree = ttk.Treeview(parent, columns=columns, show="headings", height=10)
# 设置列标题和宽度
self.tree.heading("材料名称", text="材料名称")
self.tree.heading("匹配的MaterialName", text="匹配的MaterialName")
self.tree.heading("负责人", text="负责人")
self.tree.heading("匹配状态", text="匹配状态")
self.tree.column("材料名称", width=300)
self.tree.column("匹配的MaterialName", width=300)
self.tree.column("负责人", width=150)
self.tree.column("匹配状态", width=100)
# 添加滚动条
scrollbar_y = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.tree.yview)
scrollbar_x = ttk.Scrollbar(parent, orient=tk.HORIZONTAL, command=self.tree.xview)
self.tree.configure(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
# 布局
self.tree.grid(row=0, column=0, sticky="nsew")
scrollbar_y.grid(row=0, column=1, sticky="ns")
scrollbar_x.grid(row=1, column=0, sticky="ew")
parent.rowconfigure(0, weight=1)
parent.columnconfigure(0, weight=1)
def _create_log_panel(self, parent):
"""创建日志面板"""
self.log_text = LogText(parent, height=8, readonly=True)
self.log_text.pack(fill=tk.BOTH, expand=True)
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")
def start_validation(self):
"""开始校验"""
# 验证输入
output_file = self.output_file_selector.get()
if not output_file:
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()
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)
if output_dir and not os.path.exists(output_dir):
os.makedirs(output_dir, exist_ok=True)
# 更新 UI 状态
self.validating = True
self.start_button.config(state=tk.DISABLED)
self.log_text.clear()
self.log_text.info("开始物料校验...")
# 清空结果表格
for item in self.tree.get_children():
self.tree.delete(item)
# 在后台线程中执行校验
validation_thread = threading.Thread(
target=self._validation_worker,
args=(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):
"""校验工作线程"""
try:
# 导入校验器
from utils.material_status_validator import MaterialStatusValidator
# 创建校验器实例(需要 ERP 凭据,因为可能需要登录系统)
validator = MaterialStatusValidator(
username=self.config.get('erp.username'),
password=self.config.get('erp.password'),
headless=self.config.get('browser.headless', True),
verbose=True
)
# 捕获 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
)
else:
result = validator.validate(
production_id_file=production_id_file,
merged_excel_file=None, # 将在内部生成
output_file=output_file
)
# 获取捕获的输出并显示到日志
output_text = captured_output.getvalue()
if output_text:
for line in output_text.split('\n'):
if line.strip():
self._update_log(line, "INFO")
if result:
self._update_log(f"校验完成,结果已保存到:{output_file}", "SUCCESS")
# 加载结果显示
self._load_results(output_file)
else:
self._update_log("校验失败", "ERROR")
except Exception as e:
self._update_log(f"校验过程中发生错误:{str(e)}", "ERROR")
finally:
# 更新 UI 状态
self.after(0, self._validation_complete)
def _validation_complete(self):
"""校验完成后的 UI 更新"""
self.validating = False
self.start_button.config(state=tk.NORMAL)
def _load_results(self, file_path: str):
"""加载校验结果到表格"""
try:
df = pd.read_excel(file_path)
# 在主线程中更新表格
def update_table():
for _, row in df.iterrows():
self.tree.insert("", tk.END, values=(
row.get('材料名称', ''),
row.get('匹配的MaterialName', ''),
row.get('负责人', ''),
row.get('匹配状态', '')
))
if len(df) > 0:
self.export_button.config(state=tk.NORMAL)
self._update_log(f"已加载 {len(df)} 条结果", "INFO")
self.after(0, update_table)
except Exception as e:
self._update_log(f"加载结果失败:{str(e)}", "ERROR")
def _update_log(self, message: str, level: str = "INFO"):
"""线程安全的日志更新"""
def update():
if level == "INFO":
self.log_text.info(message)
elif level == "SUCCESS":
self.log_text.success(message)
elif level == "WARNING":
self.log_text.warning(message)
elif level == "ERROR":
self.log_text.error(message)
self.after(0, update)
def export_results(self):
"""导出结果到 Excel"""
output_file = self.output_file_selector.get()
if not output_file:
output_file = filedialog.asksaveasfilename(
title="保存结果",
defaultextension=".xlsx",
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")]
)
if not output_file:
return
try:
# 收集表格数据
data = []
for item in self.tree.get_children():
values = self.tree.item(item)['values']
data.append(values)
if not data:
messagebox.showwarning("警告", "没有数据可导出")
return
# 创建 DataFrame 并保存
df = pd.DataFrame(data, columns=["材料名称", "匹配的MaterialName", "负责人", "匹配状态"])
df.to_excel(output_file, index=False)
messagebox.showinfo("成功", f"结果已导出到:{output_file}")
self.log_text.success(f"结果已导出到:{output_file}")
except Exception as e:
messagebox.showerror("错误", f"导出失败:{str(e)}")
self.log_text.error(f"导出失败:{str(e)}")