style: format all Python files with Black

Apply Black formatter to the entire codebase for consistent code style.

Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-02-26 22:44:03 +08:00
parent 1b16842a2c
commit 3b7c00377f
46 changed files with 1488 additions and 974 deletions

View File

@@ -19,13 +19,13 @@ from .constants import (
from .utils import admin_only, require_session
__all__ = [
'BaseTab',
'WINDOW_SIZE',
'MIN_WINDOW_SIZE',
'POLL_INTERVAL_MS',
'LOG_COLORS',
'DEFAULT_FONT_FAMILY',
'DEFAULT_FONT_SIZE',
'admin_only',
'require_session',
"BaseTab",
"WINDOW_SIZE",
"MIN_WINDOW_SIZE",
"POLL_INTERVAL_MS",
"LOG_COLORS",
"DEFAULT_FONT_FAMILY",
"DEFAULT_FONT_SIZE",
"admin_only",
"require_session",
]

View File

@@ -48,7 +48,7 @@ class BaseTab(ttk.Frame):
如果 self.logger 未设置,将使用 logging.getLogger(__name__) 作为后备。
"""
# 获取 logger优先使用实例的 logger否则使用模块 logger
logger = getattr(self, 'logger', None) or logging.getLogger(__name__)
logger = getattr(self, "logger", None) or logging.getLogger(__name__)
# 将自定义级别映射到 logging 级别
level_upper = level.upper()
@@ -83,11 +83,11 @@ class BaseTab(ttk.Frame):
需要 main_window 或 session_manager 支持。
"""
# 尝试从 session_manager 获取
if hasattr(self, 'session_manager') and self.session_manager:
if hasattr(self, "session_manager") and self.session_manager:
return self.session_manager.is_admin()
# 尝试从 main_window 获取
if self.main_window and hasattr(self.main_window, 'session_manager'):
if self.main_window and hasattr(self.main_window, "session_manager"):
return self.main_window.session_manager.is_admin()
return False
@@ -101,10 +101,10 @@ class BaseTab(ttk.Frame):
Note:
需要 session_manager 支持。
"""
if hasattr(self, 'session_manager') and self.session_manager:
if hasattr(self, "session_manager") and self.session_manager:
return self.session_manager.get_username() or ""
if self.main_window and hasattr(self.main_window, 'session_manager'):
if self.main_window and hasattr(self.main_window, "session_manager"):
return self.main_window.session_manager.get_username() or ""
return ""
@@ -128,5 +128,5 @@ class BaseTab(ttk.Frame):
def reload_config(self):
"""重新加载配置(子类可覆盖此方法)"""
if self.config and hasattr(self.config, 'reload'):
self.config.reload()
if self.config and hasattr(self.config, "reload"):
self.config.reload()

View File

@@ -6,6 +6,7 @@
负责加载、保存和管理用户配置。
支持从环境变量和 .env 文件加载配置。
"""
import os
from typing import TYPE_CHECKING
from config.loader import ConfigLoader
@@ -20,7 +21,9 @@ if TYPE_CHECKING:
class ConfigManager:
"""配置管理器"""
def __init__(self, config_file: str = "config/user_settings.json", use_env: bool = True):
def __init__(
self, config_file: str = "config/user_settings.json", use_env: bool = True
):
"""
初始化配置管理器
@@ -99,7 +102,9 @@ class ConfigManager:
field_type = type(getattr(obj, target_field))
# 如果是字符串且目标字段是枚举类型,进行转换
if isinstance(value, str) and hasattr(field_type, "__members__"): # 它是一个 Enum
if isinstance(value, str) and hasattr(
field_type, "__members__"
): # 它是一个 Enum
try:
value = field_type(value)
except ValueError:

View File

@@ -45,11 +45,11 @@ HEADING_STORE_DELAY_MS = 100
# 日志级别对应的颜色
LOG_COLORS = {
"INFO": "#000000", # 黑色
"SUCCESS": "#008000", # 绿色
"WARNING": "#FF8C00", # 橙色
"ERROR": "#FF0000", # 红色
"DEBUG": "#808080", # 灰色
"INFO": "#000000", # 黑色
"SUCCESS": "#008000", # 绿色
"WARNING": "#FF8C00", # 橙色
"ERROR": "#FF0000", # 红色
"DEBUG": "#808080", # 灰色
}
# ============================================================================
@@ -109,4 +109,4 @@ DEFAULT_DB_BATCH_SIZE = 2000
CHECKBOX_CHECKED = ""
# 未选中状态
CHECKBOX_UNCHECKED = ""
CHECKBOX_UNCHECKED = ""

View File

@@ -70,10 +70,12 @@ class DataExtractionTab(BaseTab):
input_group.pack(fill=tk.BOTH, expand=True)
self.production_id_input = ProductionIdInput(
input_group,
placeholder="每行输入一个 Production ID\n\n示例:\n26B848\n26B849"
placeholder="每行输入一个 Production ID\n\n示例:\n26B848\n26B849",
)
self.production_id_input.pack(fill=tk.BOTH, expand=True)
self.production_id_input.text_widget.bind("<FocusOut>", self._on_production_ids_changed)
self.production_id_input.text_widget.bind(
"<FocusOut>", self._on_production_ids_changed
)
def _create_right_panel(self, parent):
main_paned = ttk.PanedWindow(parent, orient=tk.VERTICAL)
@@ -89,7 +91,9 @@ class DataExtractionTab(BaseTab):
output_group = ttk.LabelFrame(parent, text="输出文件", padding=10)
output_group.pack(fill=tk.X, pady=5)
self.output_file_selector = FileSelector(
output_group, label_text="保存为:", file_type="file",
output_group,
label_text="保存为:",
file_type="file",
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
initial_dir=self.config.get("paths.data_dir", "data/"),
)
@@ -103,20 +107,28 @@ class DataExtractionTab(BaseTab):
options_group = ttk.LabelFrame(parent, text="提取选项", padding=10)
options_group.pack(fill=tk.X, pady=5)
self.headless_var = tk.BooleanVar(value=self.config.get("erp.headless", True))
ttk.Checkbutton(options_group, text="无头模式", variable=self.headless_var).grid(row=0, column=0, sticky="w", padx=5)
ttk.Checkbutton(
options_group, text="无头模式", variable=self.headless_var
).grid(row=0, column=0, sticky="w", padx=5)
progress_group = ttk.LabelFrame(parent, text="进度", padding=10)
progress_group.pack(fill=tk.X, pady=5)
self.progress_bar = ttk.Progressbar(progress_group, mode="determinate")
self.progress_bar.pack(fill=tk.X, pady=5)
self.status_label = ttk.Label(progress_group, text="就绪", relief=tk.SUNKEN, anchor=tk.W)
self.status_label = ttk.Label(
progress_group, text="就绪", relief=tk.SUNKEN, anchor=tk.W
)
self.status_label.pack(fill=tk.X)
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, pady=10)
self.start_button = ttk.Button(button_frame, text="开始提取", command=self.start_extraction)
self.start_button = ttk.Button(
button_frame, text="开始提取", command=self.start_extraction
)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(button_frame, text="停止", command=self.stop_extraction, state=tk.DISABLED)
self.stop_button = ttk.Button(
button_frame, text="停止", command=self.stop_extraction, state=tk.DISABLED
)
self.stop_button.pack(side=tk.LEFT, padx=5)
def _create_log_panel(self, parent):
@@ -125,10 +137,11 @@ class DataExtractionTab(BaseTab):
# 设置 GUI 日志处理器,将 logging 输出桥接到 LogText 组件
self._gui_handler = GuiTextHandler(self.log_text)
self._gui_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
self._gui_handler.setFormatter(
logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
)
self.logger.addHandler(self._gui_handler)
def _apply_ui_config(self):
@@ -136,7 +149,7 @@ class DataExtractionTab(BaseTab):
font_family = self.config.get("ui.font_family", "Microsoft YaHei UI")
font_size = self.config.get("ui.font_size", 10)
self.production_id_input.apply_font(font_family, font_size)
if hasattr(self.log_text, 'apply_font'):
if hasattr(self.log_text, "apply_font"):
self.log_text.apply_font(font_family, font_size)
except Exception as e:
self.logger.debug(f"应用 UI 配置失败: {e}")
@@ -163,7 +176,9 @@ class DataExtractionTab(BaseTab):
self.status_label.config(text="正在初始化...")
self.log_text.clear()
self.extraction_thread = threading.Thread(
target=self._extraction_worker, args=(production_ids, output_file), daemon=True
target=self._extraction_worker,
args=(production_ids, output_file),
daemon=True,
)
self.extraction_thread.start()
@@ -174,20 +189,28 @@ class DataExtractionTab(BaseTab):
def _extraction_worker(self, production_ids: list[str], output_file: str):
import tempfile
temp_file = None
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
temp_file = f.name
f.write('\n'.join(production_ids))
f.write("\n".join(production_ids))
from utils.discrete_material_plan_extractor import (
DiscreteMaterialPlanExtractor,
)
from utils.discrete_material_plan_extractor import DiscreteMaterialPlanExtractor
self.extractor = DiscreteMaterialPlanExtractor(
username=self.config.get("erp.username"),
password=self.config.get("erp.password"),
headless=self.headless_var.get(),
verbose=self.config.get("extraction.verbose", True),
batch_size=self.config.get("extraction.batch_size", 100),
enable_db_persistence=self.config.get("extraction.enable_db_persistence", False),
enable_db_persistence=self.config.get(
"extraction.enable_db_persistence", False
),
)
# 修复:直接调用标准的 _update_log不再传入 add_timestamp 参数
@@ -196,11 +219,15 @@ class DataExtractionTab(BaseTab):
level = progress_info.detail.get("log_level", "INFO").upper()
self._update_log(progress_info.message, level)
else:
percent = self.progress_calculator.calculate_overall_percent(progress_info)
percent = self.progress_calculator.calculate_overall_percent(
progress_info
)
self._update_progress(percent, progress_info.message)
result = self.extractor.extract(
production_id_file=temp_file, output_file=output_file, progress_callback=progress_callback
production_id_file=temp_file,
output_file=output_file,
progress_callback=progress_callback,
)
if result and self.extracting:
@@ -246,8 +273,10 @@ class DataExtractionTab(BaseTab):
def _on_production_ids_changed(self, event=None):
if self.main_window:
self.main_window.update_shared_production_ids(self.production_id_input.get())
self.main_window.update_shared_production_ids(
self.production_id_input.get()
)
def reload_config(self):
self._apply_ui_config()
self._on_production_ids_changed()
self._on_production_ids_changed()

View File

@@ -4,11 +4,12 @@
GUI 日志配置模块
统一配置 GUI 应用和控制台的日志输出
"""
import logging
# 日志格式配置
LOG_FORMAT = '%(asctime)s [%(levelname)s] %(message)s'
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
def setup_gui_logging(level=logging.INFO):
@@ -25,7 +26,7 @@ def setup_gui_logging(level=logging.INFO):
level=level,
format=LOG_FORMAT,
datefmt=DATE_FORMAT,
force=True # 确保重新配置(即使之前配置过)
force=True, # 确保重新配置(即使之前配置过)
)
return logging.getLogger()

View File

@@ -1,6 +1,7 @@
"""
Login Dialog - Modal dialog for user authentication
"""
import socket
import tkinter as tk
from tkinter import ttk, messagebox
@@ -62,7 +63,7 @@ class LoginDialog:
self._create_widgets()
# Bind Enter key to login button
self.dialog.bind('<Return>', lambda e: self._on_login())
self.dialog.bind("<Return>", lambda e: self._on_login())
# Focus on username entry
self.username_entry.focus_set()
@@ -74,19 +75,15 @@ class LoginDialog:
main_frame.pack(fill=tk.BOTH, expand=True)
# Title
title_label = ttk.Label(
main_frame,
text="请登录",
font=('', 16, 'bold')
)
title_label = ttk.Label(main_frame, text="请登录", font=("", 16, "bold"))
title_label.pack(pady=(0, 10))
# Computer name display
computer_name_label = ttk.Label(
main_frame,
text=f"当前计算机: {socket.gethostname()}",
font=('', 9),
foreground='gray'
font=("", 9),
foreground="gray",
)
computer_name_label.pack(pady=(0, 15))
@@ -112,28 +109,19 @@ class LoginDialog:
# Login button
login_btn = ttk.Button(
button_frame,
text="登录",
command=self._on_login,
width=10
button_frame, text="登录", command=self._on_login, width=10
)
login_btn.pack(side=tk.LEFT, padx=5)
# Cancel button
cancel_btn = ttk.Button(
button_frame,
text="取消",
command=self._on_cancel,
width=10
button_frame, text="取消", command=self._on_cancel, width=10
)
cancel_btn.pack(side=tk.LEFT, padx=5)
# Version info
version_label = ttk.Label(
main_frame,
text="v1.0",
font=('', 8),
foreground='gray'
main_frame, text="v1.0", font=("", 8), foreground="gray"
)
version_label.pack(side=tk.BOTTOM, pady=10)

View File

@@ -36,7 +36,9 @@ class MainWindow:
# 设置窗口属性(包含用户信息)
user_type_display = "管理员" if session_manager.is_admin() else "用户"
self.root.title(f"ERP 自动化工具 v1.0 - {session_manager.get_username()} ({user_type_display})")
self.root.title(
f"ERP 自动化工具 v1.0 - {session_manager.get_username()} ({user_type_display})"
)
self.root.geometry("1000x700")
# 设置最小窗口大小
@@ -62,8 +64,8 @@ class MainWindow:
"""更新共享的 Production ID 列表"""
self.shared_production_ids = production_ids
# 通知物料校验标签页 Production ID 已更新
if hasattr(self, 'validation_tab'):
if hasattr(self.validation_tab, 'on_production_ids_updated'):
if hasattr(self, "validation_tab"):
if hasattr(self.validation_tab, "on_production_ids_updated"):
self.validation_tab.on_production_ids_updated(production_ids)
def create_menu(self):
@@ -92,11 +94,15 @@ class MainWindow:
self.notebook.add(self.extraction_tab, text="数据提取")
# 物料校验标签页(传入 session_manager 和 main_window
self.validation_tab = MaterialValidationTab(self.notebook, self.config, self.session_manager, self)
self.validation_tab = MaterialValidationTab(
self.notebook, self.config, self.session_manager, self
)
self.notebook.add(self.validation_tab, text="物料校验")
# 设置标签页(传入 session_manager
self.settings_tab = SettingsTab(self.notebook, self.config, self.session_manager)
self.settings_tab = SettingsTab(
self.notebook, self.config, self.session_manager
)
self.notebook.add(self.settings_tab, text="设置")
# 初始化:如果数据提取页面已有 Production ID通知物料校验页面
@@ -105,7 +111,7 @@ class MainWindow:
def _initialize_shared_production_ids(self):
"""初始化共享的 Production ID从数据提取页面获取"""
try:
if hasattr(self.extraction_tab, 'production_id_input'):
if hasattr(self.extraction_tab, "production_id_input"):
production_ids = self.extraction_tab.production_id_input.get()
if production_ids:
self.update_shared_production_ids(production_ids)
@@ -134,7 +140,9 @@ class MainWindow:
user_info_text = f"当前用户: {self.session_manager.get_username()} ({user_type_display}) - 以 {original_admin['username']} 身份登录"
else:
# 正常登录
user_info_text = f"当前用户: {self.session_manager.get_username()} ({user_type_display})"
user_info_text = (
f"当前用户: {self.session_manager.get_username()} ({user_type_display})"
)
self.user_info_var = tk.StringVar()
self.user_info_var.set(user_info_text)
@@ -181,9 +189,9 @@ class MainWindow:
self.config.reload()
# 通知各个标签页重新加载配置
if hasattr(self.extraction_tab, 'reload_config'):
if hasattr(self.extraction_tab, "reload_config"):
self.extraction_tab.reload_config()
if hasattr(self.validation_tab, 'reload_config'):
if hasattr(self.validation_tab, "reload_config"):
self.validation_tab.reload_config()
# 更新状态栏

View File

@@ -62,34 +62,22 @@ class ResultDialog(tk.Toplevel):
text="",
font=("Arial", 48),
fg="#22c55e", # 绿色
bg="#f0fdf4" # 浅绿背景
bg="#f0fdf4", # 浅绿背景
)
icon_label.pack(side=tk.LEFT, padx=(0, 20))
# 背景框
icon_frame = tk.Frame(
content_frame,
bg="#f0fdf4",
width=80,
height=80
)
icon_frame = tk.Frame(content_frame, bg="#f0fdf4", width=80, height=80)
icon_frame.place(x=0, y=0)
icon_frame.pack_propagate(False)
icon_label = tk.Label(
icon_frame,
text="",
font=("Arial", 48),
fg="#22c55e",
bg="#f0fdf4"
icon_frame, text="", font=("Arial", 48), fg="#22c55e", bg="#f0fdf4"
)
icon_label.place(relx=0.5, rely=0.5, anchor="center")
else:
# 失败图标:红色叉叉
icon_frame = tk.Frame(
content_frame,
bg="#fef2f2", # 浅红背景
width=80,
height=80
content_frame, bg="#fef2f2", width=80, height=80 # 浅红背景
)
icon_frame.pack_propagate(False)
icon_frame.pack(side=tk.LEFT, padx=(0, 20))
@@ -99,7 +87,7 @@ class ResultDialog(tk.Toplevel):
text="",
font=("Arial", 48),
fg="#ef4444", # 红色
bg="#fef2f2"
bg="#fef2f2",
)
icon_label.place(relx=0.5, rely=0.5, anchor="center")
@@ -109,7 +97,7 @@ class ResultDialog(tk.Toplevel):
text=message,
font=("Microsoft YaHei UI", 10),
justify=tk.LEFT,
wraplength=280
wraplength=280,
)
msg_label.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
@@ -117,12 +105,9 @@ class ResultDialog(tk.Toplevel):
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(10, 0))
ttk.Button(
button_frame,
text="确定",
command=self.destroy,
width=10
).pack(side=tk.RIGHT)
ttk.Button(button_frame, text="确定", command=self.destroy, width=10).pack(
side=tk.RIGHT
)
# 等待窗口关闭
self.wait_window()
@@ -260,7 +245,9 @@ class EditableTreeview(ttk.Treeview):
new_value = self.edit_entry.get()
values = self.item(self.editing_item, "values")
old_value = values[self.editing_column] if self.editing_column < len(values) else ""
old_value = (
values[self.editing_column] if self.editing_column < len(values) else ""
)
# 销毁 Entry先销毁防止重复触发
entry = self.edit_entry
@@ -276,7 +263,9 @@ class EditableTreeview(ttk.Treeview):
# 调用回调
try:
if self.on_edit_complete:
self.on_edit_complete(editing_item, editing_column, old_value, new_value)
self.on_edit_complete(
editing_item, editing_column, old_value, new_value
)
except Exception as e:
# 回调出错,清除状态
print(f"Error in on_edit_complete: {e}")
@@ -287,7 +276,9 @@ class EditableTreeview(ttk.Treeview):
return
values = self.item(self.editing_item, "values")
old_value = values[self.editing_column] if self.editing_column < len(values) else ""
old_value = (
values[self.editing_column] if self.editing_column < len(values) else ""
)
# 销毁 Entry先保存状态引用
entry = self.edit_entry
@@ -361,9 +352,11 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 数据缓存
self.original_data: List[Dict[str, Any]] = [] # 原始数据(从数据库加载)
self.current_data: List[Dict[str, Any]] = [] # 当前显示的数据
self.current_data: List[Dict[str, Any]] = [] # 当前显示的数据
self.row_status: Dict[str, str] = {} # key -> 行状态
self.original_values: Dict[str, Tuple[str, str]] = {} # key -> (original_material, original_manager) 用于修改
self.original_values: Dict[str, Tuple[str, str]] = (
{}
) # key -> (original_material, original_manager) 用于修改
# 筛选相关
self.managers: List[str] = []
@@ -387,12 +380,18 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 顶部:筛选区域 - 仅管理员可见
if self.session_manager.is_admin():
filter_frame = ttk.LabelFrame(main_container, text="筛选(按负责人)", padding=10)
filter_frame = ttk.LabelFrame(
main_container, text="筛选(按负责人)", padding=10
)
filter_frame.pack(fill=tk.X, pady=(0, 10))
self._create_filter_area(filter_frame)
# 中部:数据表格
table_frame = ttk.LabelFrame(main_container, text="数据列表双击编辑Delete删除Insert新增", padding=10)
table_frame = ttk.LabelFrame(
main_container,
text="数据列表双击编辑Delete删除Insert新增",
padding=10,
)
table_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
self._create_table(table_frame)
@@ -411,12 +410,16 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# Canvas和滚动条
self.filter_canvas = tk.Canvas(canvas_container, height=80)
scrollbar = ttk.Scrollbar(canvas_container, orient="vertical", command=self.filter_canvas.yview)
scrollbar = ttk.Scrollbar(
canvas_container, orient="vertical", command=self.filter_canvas.yview
)
self.filter_frame = ttk.Frame(self.filter_canvas)
self.filter_frame.bind(
"<Configure>",
lambda e: self.filter_canvas.configure(scrollregion=self.filter_frame.bbox("all"))
lambda e: self.filter_canvas.configure(
scrollregion=self.filter_frame.bbox("all")
),
)
self.filter_canvas.create_window((0, 0), window=self.filter_frame, anchor="nw")
@@ -428,15 +431,20 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 鼠标滚轮支持
def _on_mousewheel(event):
self.filter_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
self.filter_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
self.filter_canvas.bind("<MouseWheel>", _on_mousewheel)
# 快捷按钮
button_frame = ttk.Frame(parent)
button_frame.pack(fill=tk.X, pady=(5, 0))
ttk.Button(button_frame, text="全选", command=self._select_all_managers).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="取消全选", command=self._deselect_all_managers).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="全选", command=self._select_all_managers).pack(
side=tk.LEFT, padx=5
)
ttk.Button(
button_frame, text="取消全选", command=self._deselect_all_managers
).pack(side=tk.LEFT, padx=5)
def _create_table(self, parent):
"""创建数据表格"""
@@ -447,7 +455,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
show="headings",
selectmode="extended",
on_edit_complete=self._on_edit_complete,
on_edit_cancelled=self._on_edit_cancelled
on_edit_cancelled=self._on_edit_cancelled,
)
# 设置列标题和宽度
@@ -459,11 +467,12 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 添加滚动条
scrollbar_y = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.tree.yview)
scrollbar_x = ttk.Scrollbar(parent, orient=tk.HORIZONTAL, command=self.tree.xview)
scrollbar_x = ttk.Scrollbar(
parent, orient=tk.HORIZONTAL, command=self.tree.xview
)
self.tree.configure(
yscrollcommand=scrollbar_y.set,
xscrollcommand=scrollbar_x.set
yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set
)
# 布局
@@ -480,18 +489,30 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 右键菜单
self.context_menu = tk.Menu(self.tree, tearoff=0)
self.context_menu.add_command(label="新增记录 (Insert)", command=self._add_new_row)
self.context_menu.add_command(label="编辑记录 (F2)", command=self._edit_selected_cell)
self.context_menu.add_command(
label="新增记录 (Insert)", command=self._add_new_row
)
self.context_menu.add_command(
label="编辑记录 (F2)", command=self._edit_selected_cell
)
self.context_menu.add_separator()
self.context_menu.add_command(label="删除记录 (Delete)", command=self._delete_selected_rows)
self.context_menu.add_command(
label="删除记录 (Delete)", command=self._delete_selected_rows
)
self.tree.bind("<Button-3>", self._show_context_menu)
def _create_buttons(self, parent):
"""创建底部按钮"""
ttk.Button(parent, text="新增 (Insert)", command=self._add_new_row).pack(side=tk.LEFT, padx=5)
ttk.Button(parent, text="删除 (Delete)", command=self._delete_selected_rows).pack(side=tk.LEFT, padx=5)
ttk.Button(parent, text="刷新", command=self._load_data).pack(side=tk.LEFT, padx=5)
ttk.Button(parent, text="新增 (Insert)", command=self._add_new_row).pack(
side=tk.LEFT, padx=5
)
ttk.Button(
parent, text="删除 (Delete)", command=self._delete_selected_rows
).pack(side=tk.LEFT, padx=5)
ttk.Button(parent, text="刷新", command=self._load_data).pack(
side=tk.LEFT, padx=5
)
# 待保存提示
self.status_label = ttk.Label(parent, text="")
@@ -501,8 +522,12 @@ class MaterialTypeManagementDialog(tk.Toplevel):
right_frame = ttk.Frame(parent)
right_frame.pack(side=tk.RIGHT)
ttk.Button(right_frame, text="保存", command=self._save_changes).pack(side=tk.LEFT, padx=5)
ttk.Button(right_frame, text="关闭", command=self._close_dialog).pack(side=tk.LEFT, padx=5)
ttk.Button(right_frame, text="保存", command=self._save_changes).pack(
side=tk.LEFT, padx=5
)
ttk.Button(right_frame, text="关闭", command=self._close_dialog).pack(
side=tk.LEFT, padx=5
)
def _show_context_menu(self, event):
"""显示右键菜单"""
@@ -525,6 +550,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 管理员:获取所有负责人
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
dao = MaterialsTypeToBeDeletedDAO()
self.managers = dao.get_managers()
@@ -539,7 +565,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
self.filter_frame,
text="全选",
variable=self.select_all_var,
command=self._on_select_all_toggle
command=self._on_select_all_toggle,
).grid(row=0, column=0, sticky="w", padx=5, pady=2)
# 负责人复选框
@@ -554,7 +580,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
self.filter_frame,
text=manager,
variable=var,
command=self._on_manager_checkbox_change
command=self._on_manager_checkbox_change,
).grid(row=row, column=col, sticky="w", padx=5, pady=2)
def _on_select_all_toggle(self):
@@ -587,8 +613,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
def _get_selected_managers(self) -> List[str]:
"""获取选中的负责人列表"""
return [
manager for manager, var in self.manager_checkboxes.items()
if var.get()
manager for manager, var in self.manager_checkboxes.items() if var.get()
]
def _load_data(self):
@@ -603,7 +628,9 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# PERMISSION CHECK: 非管理员用户只加载自己的数据
if not self.session_manager.is_admin():
self.original_data = dao.get_materials_by_manager(self.session_manager.get_username())
self.original_data = dao.get_materials_by_manager(
self.session_manager.get_username()
)
else:
self.original_data = dao.get_all_materials()
@@ -653,8 +680,12 @@ class MaterialTypeManagementDialog(tk.Toplevel):
else:
# 获取未删除的数据
base_data = [
r for r in self.original_data
if self.row_status.get(self._get_record_key(r), self.ROW_STATUS_UNCHANGED) != self.ROW_STATUS_DELETED
r
for r in self.original_data
if self.row_status.get(
self._get_record_key(r), self.ROW_STATUS_UNCHANGED
)
!= self.ROW_STATUS_DELETED
]
# 添加新增的记录
@@ -665,18 +696,16 @@ class MaterialTypeManagementDialog(tk.Toplevel):
try:
if self.tree.exists(item_id):
values = self.tree.item(item_id, "values")
new_records.append({
'MaterialName': values[0],
'ManagerName': values[1]
})
new_records.append(
{"MaterialName": values[0], "ManagerName": values[1]}
)
except:
pass
# 合并数据并筛选
all_data = base_data + new_records
self.current_data = [
r for r in all_data
if r.get('ManagerName') in selected_managers
r for r in all_data if r.get("ManagerName") in selected_managers
]
self._refresh_tree()
@@ -714,10 +743,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
item_id = self.tree.insert(
"",
tk.END,
values=(
record.get('MaterialName', ''),
record.get('ManagerName', '')
)
values=(record.get("MaterialName", ""), record.get("ManagerName", "")),
)
# 恢复行状态
@@ -725,9 +751,9 @@ class MaterialTypeManagementDialog(tk.Toplevel):
if key in self.row_status:
status = self.row_status[key]
if status == self.ROW_STATUS_NEW:
self.tree.item(item_id, tags=('new',))
self.tree.item(item_id, tags=("new",))
elif status == self.ROW_STATUS_MODIFIED:
self.tree.item(item_id, tags=('modified',))
self.tree.item(item_id, tags=("modified",))
# 恢复正在编辑的新增行
for temp_key, values in editing_data.items():
@@ -735,17 +761,19 @@ class MaterialTypeManagementDialog(tk.Toplevel):
new_item_id = self.tree.insert("", tk.END, values=values)
# 更新 key 映射
if values[0] and values[1]:
new_key = self._get_record_key({'MaterialName': values[0], 'ManagerName': values[1]})
new_key = self._get_record_key(
{"MaterialName": values[0], "ManagerName": values[1]}
)
self.row_status[new_key] = self.ROW_STATUS_NEW
self.tree.item(new_item_id, tags=('new',))
self.tree.item(new_item_id, tags=("new",))
else:
# 空行,保持临时 key
self.row_status[temp_key] = self.ROW_STATUS_NEW
self.row_status[new_key] = self.ROW_STATUS_NEW
# 配置标签样式
self.tree.tag_configure('new', background='#e6f7e6') # 浅绿色
self.tree.tag_configure('modified', background='#fff4e6') # 浅黄色
self.tree.tag_configure("new", background="#e6f7e6") # 浅绿色
self.tree.tag_configure("modified", background="#fff4e6") # 浅黄色
# 恢复选中状态
for material, manager in selected_data:
@@ -765,7 +793,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
self.row_status[temp_key] = self.ROW_STATUS_NEW
# 设置标签
self.tree.item(item_id, tags=('new',))
self.tree.item(item_id, tags=("new",))
# 选中并开始编辑第一个单元格
self.tree.selection_set(item_id)
@@ -787,7 +815,9 @@ class MaterialTypeManagementDialog(tk.Toplevel):
if not selection:
return
if not messagebox.askyesno("确认", f"确定要删除选中的 {len(selection)} 条记录吗?"):
if not messagebox.askyesno(
"确认", f"确定要删除选中的 {len(selection)} 条记录吗?"
):
return
for item in selection:
@@ -795,11 +825,16 @@ class MaterialTypeManagementDialog(tk.Toplevel):
material_name = values[0]
manager_name = values[1]
key = self._get_record_key({'MaterialName': material_name, 'ManagerName': manager_name})
key = self._get_record_key(
{"MaterialName": material_name, "ManagerName": manager_name}
)
temp_key = f"temp:{item}"
# 如果是新增的行,直接移除
if temp_key in self.row_status and self.row_status[temp_key] == self.ROW_STATUS_NEW:
if (
temp_key in self.row_status
and self.row_status[temp_key] == self.ROW_STATUS_NEW
):
del self.row_status[temp_key]
else:
# 标记为删除
@@ -809,7 +844,9 @@ class MaterialTypeManagementDialog(tk.Toplevel):
self._update_status()
def _on_edit_complete(self, item_id: str, column: int, old_value: str, new_value: str):
def _on_edit_complete(
self, item_id: str, column: int, old_value: str, new_value: str
):
"""编辑完成回调"""
new_value = new_value.strip()
@@ -825,7 +862,10 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 检查是否是新增的行
temp_key = f"temp:{item_id}"
is_new_row = temp_key in self.row_status and self.row_status[temp_key] == self.ROW_STATUS_NEW
is_new_row = (
temp_key in self.row_status
and self.row_status[temp_key] == self.ROW_STATUS_NEW
)
# 对于新增行,如果只输入了部分字段,允许继续
if is_new_row:
@@ -834,13 +874,18 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 检查是否已输入完整数据
if new_material and new_manager:
# 输入完整,更新 key
new_key = self._get_record_key({'MaterialName': new_material, 'ManagerName': new_manager})
new_key = self._get_record_key(
{"MaterialName": new_material, "ManagerName": new_manager}
)
# 检查重复
for other_item in self.tree.get_children():
if other_item == item_id:
continue
other_values = self.tree.item(other_item, "values")
if other_values[0] == new_material and other_values[1] == new_manager:
if (
other_values[0] == new_material
and other_values[1] == new_manager
):
messagebox.showwarning("警告", "该记录已存在")
self.tree.item(item_id, values=("", ""))
self._update_status()
@@ -864,7 +909,9 @@ class MaterialTypeManagementDialog(tk.Toplevel):
self.tree.item(item_id, values=new_values)
return
new_key = self._get_record_key({'MaterialName': new_material, 'ManagerName': new_manager})
new_key = self._get_record_key(
{"MaterialName": new_material, "ManagerName": new_manager}
)
# 检查是否重复(除了自己)
for other_item in self.tree.get_children():
@@ -883,7 +930,10 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 更新行状态
# 检查是否是新增的行(查找临时 key
temp_key = f"temp:{item_id}"
is_new_row = temp_key in self.row_status and self.row_status[temp_key] == self.ROW_STATUS_NEW
is_new_row = (
temp_key in self.row_status
and self.row_status[temp_key] == self.ROW_STATUS_NEW
)
if is_new_row:
# 新增行:从临时 key 更新为实际 key
@@ -904,7 +954,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
self.row_status[new_key] = self.ROW_STATUS_MODIFIED
# 设置标签
self.tree.item(item_id, tags=('modified',))
self.tree.item(item_id, tags=("modified",))
self._update_status()
@@ -926,9 +976,11 @@ class MaterialTypeManagementDialog(tk.Toplevel):
for key, status in self.row_status.items():
if status == self.ROW_STATUS_DELETED:
# 解析 key
parts = key.split('|')
parts = key.split("|")
if len(parts) == 2:
to_delete.append({'MaterialName': parts[0], 'ManagerName': parts[1]})
to_delete.append(
{"MaterialName": parts[0], "ManagerName": parts[1]}
)
elif status == self.ROW_STATUS_NEW:
# 从表格中获取数据
@@ -943,7 +995,9 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 正常 key在表格中查找匹配的行
for item in self.tree.get_children():
values = self.tree.item(item, "values")
item_key = self._get_record_key({'MaterialName': values[0], 'ManagerName': values[1]})
item_key = self._get_record_key(
{"MaterialName": values[0], "ManagerName": values[1]}
)
if item_key == key:
item_to_find = item
break
@@ -951,7 +1005,9 @@ class MaterialTypeManagementDialog(tk.Toplevel):
if item_to_find:
values = self.tree.item(item_to_find, "values")
if values[0] and values[1]: # 只保存非空行
to_insert.append({'MaterialName': values[0], 'ManagerName': values[1]})
to_insert.append(
{"MaterialName": values[0], "ManagerName": values[1]}
)
elif status == self.ROW_STATUS_MODIFIED:
# 从表格中获取新数据,从 original_values 获取旧数据
@@ -961,12 +1017,22 @@ class MaterialTypeManagementDialog(tk.Toplevel):
# 从表格中找到对应的新数据
for item in self.tree.get_children():
values = self.tree.item(item, "values")
item_key = self._get_record_key({'MaterialName': values[0], 'ManagerName': values[1]})
item_key = self._get_record_key(
{"MaterialName": values[0], "ManagerName": values[1]}
)
if item_key == key:
to_update.append({
'old': {'MaterialName': old_material, 'ManagerName': old_manager},
'new': {'MaterialName': values[0], 'ManagerName': values[1]}
})
to_update.append(
{
"old": {
"MaterialName": old_material,
"ManagerName": old_manager,
},
"new": {
"MaterialName": values[0],
"ManagerName": values[1],
},
}
)
break
total_changes = len(to_insert) + len(to_delete) + len(to_update)
@@ -985,8 +1051,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
msg_parts.append(f"更新 {len(to_update)}")
if not messagebox.askyesno(
"确认保存",
"确定要将以下更改保存到数据库吗?\n\n" + "\n".join(msg_parts)
"确认保存", "确定要将以下更改保存到数据库吗?\n\n" + "\n".join(msg_parts)
):
return
@@ -996,60 +1061,62 @@ class MaterialTypeManagementDialog(tk.Toplevel):
dao = MaterialsTypeToBeDeletedDAO()
stats = {
'insert_success': 0,
'insert_failed': 0,
'delete_success': 0,
'delete_failed': 0,
'update_success': 0,
'update_failed': 0
"insert_success": 0,
"insert_failed": 0,
"delete_success": 0,
"delete_failed": 0,
"update_success": 0,
"update_failed": 0,
}
# 执行插入
for record in to_insert:
if dao.insert_material(record['MaterialName'], record['ManagerName']):
stats['insert_success'] += 1
if dao.insert_material(record["MaterialName"], record["ManagerName"]):
stats["insert_success"] += 1
else:
stats['insert_failed'] += 1
stats["insert_failed"] += 1
# 执行删除
for record in to_delete:
if dao.delete_material(record['MaterialName'], record['ManagerName']):
stats['delete_success'] += 1
if dao.delete_material(record["MaterialName"], record["ManagerName"]):
stats["delete_success"] += 1
else:
stats['delete_failed'] += 1
stats["delete_failed"] += 1
# 执行更新
for update in to_update:
old = update['old']
new = update['new']
if dao.delete_material(old['MaterialName'], old['ManagerName']):
if dao.insert_material(new['MaterialName'], new['ManagerName']):
stats['update_success'] += 1
old = update["old"]
new = update["new"]
if dao.delete_material(old["MaterialName"], old["ManagerName"]):
if dao.insert_material(new["MaterialName"], new["ManagerName"]):
stats["update_success"] += 1
else:
dao.insert_material(old['MaterialName'], old['ManagerName'])
stats['update_failed'] += 1
dao.insert_material(old["MaterialName"], old["ManagerName"])
stats["update_failed"] += 1
else:
stats['update_failed'] += 1
stats["update_failed"] += 1
# 显示结果
result_parts = []
if stats['insert_success'] > 0:
if stats["insert_success"] > 0:
result_parts.append(f"新增成功:{stats['insert_success']}")
if stats['insert_failed'] > 0:
if stats["insert_failed"] > 0:
result_parts.append(f"新增失败:{stats['insert_failed']}")
if stats['delete_success'] > 0:
if stats["delete_success"] > 0:
result_parts.append(f"删除成功:{stats['delete_success']}")
if stats['delete_failed'] > 0:
if stats["delete_failed"] > 0:
result_parts.append(f"删除失败:{stats['delete_failed']}")
if stats['update_success'] > 0:
if stats["update_success"] > 0:
result_parts.append(f"更新成功:{stats['update_success']}")
if stats['update_failed'] > 0:
if stats["update_failed"] > 0:
result_parts.append(f"更新失败:{stats['update_failed']}")
result_msg = "\n".join(result_parts)
# 使用自定义对话框显示结果
has_failures = (stats['insert_failed'] + stats['delete_failed'] + stats['update_failed']) > 0
has_failures = (
stats["insert_failed"] + stats["delete_failed"] + stats["update_failed"]
) > 0
if has_failures:
ResultDialog(self, "保存完成(部分失败)", result_msg, success=False)
else:
@@ -1084,8 +1151,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
if total_changes > 0:
self.status_label.config(
text=f"{total_changes} 项待保存的更改",
foreground="red"
text=f"{total_changes} 项待保存的更改", foreground="red"
)
else:
self.status_label.config(text="")
@@ -1113,8 +1179,7 @@ class MaterialTypeManagementDialog(tk.Toplevel):
if total_changes > 0:
if not messagebox.askyesno(
"警告",
f"{total_changes} 项未保存的更改,确定要关闭吗?"
"警告", f"{total_changes} 项未保存的更改,确定要关闭吗?"
):
return

View File

@@ -19,7 +19,13 @@ from io import StringIO
from contextlib import redirect_stdout
from typing import List, Dict
from gui.base_tab import BaseTab
from gui.widgets import FileSelector, LogText, GuiTextHandler, DeleteProgressWindow, CheckboxTreeview
from gui.widgets import (
FileSelector,
LogText,
GuiTextHandler,
DeleteProgressWindow,
CheckboxTreeview,
)
from gui.config_manager import ConfigManager
from gui.log_config import setup_gui_logging, get_logger
from gui.material_type_management_dialog import MaterialTypeManagementDialog
@@ -31,7 +37,9 @@ import tempfile
class MaterialValidationTab(BaseTab):
"""物料校验标签页"""
def __init__(self, parent, config: ConfigManager, session_manager, main_window=None):
def __init__(
self, parent, config: ConfigManager, session_manager, main_window=None
):
"""
初始化物料校验标签页
@@ -52,7 +60,9 @@ class MaterialValidationTab(BaseTab):
# 负责人筛选相关
self.managers: List[str] = [] # 可用负责人列表
self.manager_checkboxes: Dict[str, tk.BooleanVar] = {} # 负责人复选框状态
self.select_all_managers_var: tk.BooleanVar = tk.BooleanVar(value=True) # 全选复选框状态
self.select_all_managers_var: tk.BooleanVar = tk.BooleanVar(
value=True
) # 全选复选框状态
self.previously_selected_managers: List[str] = [] # 保存筛选状态
# 初始化统一日志系统
@@ -83,7 +93,7 @@ class MaterialValidationTab(BaseTab):
manager_filter_frame = ttk.LabelFrame(
main_container,
text="筛选(按负责人)",
padding=(10, 10, 10, 10) # 左、上、右、下 - 底部padding减小
padding=(10, 10, 10, 10), # 左、上、右、下 - 底部padding减小
)
manager_filter_frame.pack(fill=tk.X, pady=(0, 10))
else:
@@ -135,7 +145,9 @@ class MaterialValidationTab(BaseTab):
# 右侧Production ID 数据源选择区域
self.source_frame = ttk.Frame(source_group)
self.source_frame.grid(row=0, column=1, rowspan=2, sticky="nsew", padx=(20, 0))
self.source_frame.grid(
row=0, column=1, rowspan=2, sticky="nsew", padx=(20, 0)
)
# Production ID 数据源选择
self.production_id_source_var = tk.StringVar(value="shared")
@@ -144,7 +156,7 @@ class MaterialValidationTab(BaseTab):
text="使用文件:",
variable=self.production_id_source_var,
value="file",
command=self._on_production_id_source_changed
command=self._on_production_id_source_changed,
).pack(side=tk.LEFT)
ttk.Radiobutton(
@@ -152,7 +164,7 @@ class MaterialValidationTab(BaseTab):
text="使用数据提取页面的 Production ID:",
variable=self.production_id_source_var,
value="shared",
command=self._on_production_id_source_changed
command=self._on_production_id_source_changed,
).pack(side=tk.LEFT, padx=10)
# 文件选择器(放在 source_group 下方,当选择文件时显示)
@@ -166,9 +178,7 @@ class MaterialValidationTab(BaseTab):
# 共享 Production ID 提示标签
self.shared_ids_info_label = ttk.Label(
source_group,
text="",
foreground="blue"
source_group, text="", foreground="blue"
)
# 初始时不显示
@@ -192,7 +202,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="全选",
command=lambda: self._select_all(check=True),
state=tk.DISABLED
state=tk.DISABLED,
)
self.select_all_button.pack(side=tk.LEFT, padx=5)
@@ -201,7 +211,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="取消全选",
command=lambda: self._select_all(check=False),
state=tk.DISABLED
state=tk.DISABLED,
)
self.deselect_all_button.pack(side=tk.LEFT, padx=5)
@@ -210,7 +220,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="确认删除",
command=self.confirm_deletion,
state=tk.DISABLED
state=tk.DISABLED,
)
self.confirm_delete_button.pack(side=tk.LEFT, padx=5)
@@ -219,7 +229,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="执行删除",
command=self.start_delete_execution,
state=tk.DISABLED
state=tk.DISABLED,
)
self.execute_delete_button.pack(side=tk.LEFT, padx=5)
@@ -227,9 +237,7 @@ class MaterialValidationTab(BaseTab):
if self.session_manager.is_admin():
self.dryrun_var = tk.BooleanVar(value=False)
self.dryrun_checkbox = ttk.Checkbutton(
button_frame,
text="预览模式 (不保存)",
variable=self.dryrun_var
button_frame, text="预览模式 (不保存)", variable=self.dryrun_var
)
self.dryrun_checkbox.pack(side=tk.LEFT, padx=5)
@@ -243,9 +251,7 @@ class MaterialValidationTab(BaseTab):
# 类型管理按钮
self.type_management_button = ttk.Button(
button_frame,
text="类型管理",
command=self.open_type_management
button_frame, text="类型管理", command=self.open_type_management
)
self.type_management_button.pack(side=tk.LEFT, padx=5)
@@ -275,7 +281,7 @@ class MaterialValidationTab(BaseTab):
columns=columns,
show="headings",
height=10,
on_checkbox_change=self._sync_checkbox_by_material_code
on_checkbox_change=self._sync_checkbox_by_material_code,
)
# 设置列标题和宽度
@@ -294,8 +300,12 @@ class MaterialValidationTab(BaseTab):
self.tree.column("负责人", width=150)
# 添加滚动条
scrollbar_y = ttk.Scrollbar(table_container, orient=tk.VERTICAL, command=self.tree.yview)
scrollbar_x = ttk.Scrollbar(table_container, orient=tk.HORIZONTAL, command=self.tree.xview)
scrollbar_y = ttk.Scrollbar(
table_container, orient=tk.VERTICAL, command=self.tree.yview
)
scrollbar_x = ttk.Scrollbar(
table_container, orient=tk.HORIZONTAL, command=self.tree.xview
)
self.tree.configure(
yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set
@@ -319,7 +329,7 @@ class MaterialValidationTab(BaseTab):
columns=columns,
show="headings",
height=10,
on_checkbox_change=self._sync_checkbox_by_material_code
on_checkbox_change=self._sync_checkbox_by_material_code,
)
# 设置列标题和宽度
@@ -338,7 +348,9 @@ class MaterialValidationTab(BaseTab):
self.tree.column("负责人", width=150)
# 添加滚动条
scrollbar_y = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.tree.yview)
scrollbar_y = ttk.Scrollbar(
parent, orient=tk.VERTICAL, command=self.tree.yview
)
scrollbar_x = ttk.Scrollbar(
parent, orient=tk.HORIZONTAL, command=self.tree.xview
)
@@ -367,7 +379,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="隐藏勾选",
command=self._hide_checked_items,
state=tk.DISABLED
state=tk.DISABLED,
)
self.btn_hide_checked.pack(fill=tk.X, pady=2)
@@ -375,19 +387,19 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="显示全部",
command=self._show_all_items,
state=tk.DISABLED
state=tk.DISABLED,
)
self.btn_show_all.pack(fill=tk.X, pady=2)
# 分隔线
ttk.Separator(button_frame, orient='horizontal').pack(fill=tk.X, pady=5)
ttk.Separator(button_frame, orient="horizontal").pack(fill=tk.X, pady=5)
# 全选按钮
self.select_all_button = ttk.Button(
button_frame,
text="全选",
command=lambda: self._select_all(check=True),
state=tk.DISABLED
state=tk.DISABLED,
)
self.select_all_button.pack(fill=tk.X, pady=2)
@@ -396,7 +408,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="取消全选",
command=lambda: self._select_all(check=False),
state=tk.DISABLED
state=tk.DISABLED,
)
self.deselect_all_button.pack(fill=tk.X, pady=2)
@@ -405,7 +417,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="确认删除",
command=self.confirm_deletion,
state=tk.DISABLED
state=tk.DISABLED,
)
self.confirm_delete_button.pack(fill=tk.X, pady=2)
@@ -414,7 +426,7 @@ class MaterialValidationTab(BaseTab):
button_frame,
text="执行删除",
command=self.start_delete_execution,
state=tk.DISABLED
state=tk.DISABLED,
)
self.execute_delete_button.pack(fill=tk.X, pady=2)
@@ -439,7 +451,7 @@ class MaterialValidationTab(BaseTab):
return
for item in self.hidden_items:
self.tree.move(item, '', 'end') # Restore to end of tree
self.tree.move(item, "", "end") # Restore to end of tree
count = len(self.hidden_items)
self.hidden_items.clear()
@@ -452,10 +464,11 @@ class MaterialValidationTab(BaseTab):
# 设置 GUI 日志处理器,将 logging 输出桥接到 LogText 组件
self._gui_handler = GuiTextHandler(self.log_text)
self._gui_handler.setFormatter(logging.Formatter(
'%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
self._gui_handler.setFormatter(
logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
)
self.logger.addHandler(self._gui_handler)
def _on_source_mode_change(self):
@@ -468,16 +481,18 @@ class MaterialValidationTab(BaseTab):
if mode == "database_full":
# 全表模式:隐藏所有 Production ID 相关控件
if hasattr(self, 'source_frame') and self.source_frame:
if hasattr(self, "source_frame") and self.source_frame:
self.source_frame.grid_forget()
if hasattr(self, 'db_filtered_production_id_selector'):
if hasattr(self, "db_filtered_production_id_selector"):
self.db_filtered_production_id_selector.grid_forget()
if hasattr(self, 'shared_ids_info_label'):
if hasattr(self, "shared_ids_info_label"):
self.shared_ids_info_label.grid_forget()
elif mode == "database_filtered":
# 过滤模式:显示数据源选择
if hasattr(self, 'source_frame') and self.source_frame:
self.source_frame.grid(row=0, column=1, rowspan=2, sticky="nsew", padx=(20, 0))
if hasattr(self, "source_frame") and self.source_frame:
self.source_frame.grid(
row=0, column=1, rowspan=2, sticky="nsew", padx=(20, 0)
)
# 根据 Production ID 数据源选择显示对应控件
self._on_production_id_source_changed()
@@ -491,11 +506,15 @@ class MaterialValidationTab(BaseTab):
if source == "shared":
# 使用共享 Production ID
self.db_filtered_production_id_selector.grid_forget()
self.shared_ids_info_label.grid(row=2, column=0, columnspan=2, sticky="w", pady=(5, 0))
self.shared_ids_info_label.grid(
row=2, column=0, columnspan=2, sticky="w", pady=(5, 0)
)
else:
# 使用文件
self.shared_ids_info_label.grid_forget()
self.db_filtered_production_id_selector.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(5, 0))
self.db_filtered_production_id_selector.grid(
row=2, column=0, columnspan=2, sticky="ew", pady=(5, 0)
)
def on_production_ids_updated(self, production_ids: list):
"""当数据提取页面的 Production ID 更新时调用"""
@@ -506,7 +525,7 @@ class MaterialValidationTab(BaseTab):
# User 模式:静默更新,不显示任何提示
if is_user_only:
# 启用执行删除按钮(如果有 Production ID
if production_ids and hasattr(self, 'execute_delete_button'):
if production_ids and hasattr(self, "execute_delete_button"):
self.execute_delete_button.config(state=tk.NORMAL)
return
@@ -518,19 +537,22 @@ class MaterialValidationTab(BaseTab):
preview += f" ... (共 {count} 个)"
self.shared_ids_info_label.config(text=f"📋 {preview}")
# 显示提示标签
if hasattr(self, 'shared_ids_label') and not self.shared_ids_label.winfo_ismapped():
if (
hasattr(self, "shared_ids_label")
and not self.shared_ids_label.winfo_ismapped()
):
self.shared_ids_label.pack(anchor="w", pady=(0, 5))
# 启用执行删除按钮
if hasattr(self, 'execute_delete_button'):
if hasattr(self, "execute_delete_button"):
self.execute_delete_button.config(state=tk.NORMAL)
else:
self.shared_ids_info_label.config(text="")
# 隐藏提示标签
if hasattr(self, 'shared_ids_label'):
if hasattr(self, "shared_ids_label"):
if self.shared_ids_label.winfo_ismapped():
self.shared_ids_label.pack_forget()
# 禁用执行删除按钮
if hasattr(self, 'execute_delete_button'):
if hasattr(self, "execute_delete_button"):
self.execute_delete_button.config(state=tk.DISABLED)
def reload_config(self):
@@ -557,8 +579,12 @@ class MaterialValidationTab(BaseTab):
button_frame = ttk.Frame(main_container)
button_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
ttk.Button(button_frame, text="全选", command=self._select_all_managers_button).pack(fill=tk.X, pady=2)
ttk.Button(button_frame, text="取消全选", command=self._deselect_all_managers_button).pack(fill=tk.X, pady=2)
ttk.Button(
button_frame, text="全选", command=self._select_all_managers_button
).pack(fill=tk.X, pady=2)
ttk.Button(
button_frame, text="取消全选", command=self._deselect_all_managers_button
).pack(fill=tk.X, pady=2)
# 右侧:负责人复选框区域(简化为直接使用 Frame
self.filter_frame = ttk.Frame(main_container)
@@ -579,7 +605,7 @@ class MaterialValidationTab(BaseTab):
# 不显示复选框,直接显示提示信息
ttk.Label(
self.filter_frame,
text=f"仅显示您的数据(负责人:{self.session_manager.get_username()}"
text=f"仅显示您的数据(负责人:{self.session_manager.get_username()}",
).pack(anchor="w")
return
@@ -613,7 +639,7 @@ class MaterialValidationTab(BaseTab):
self.filter_frame,
text=manager,
variable=var,
command=self._on_manager_checkbox_change
command=self._on_manager_checkbox_change,
).grid(row=row, column=col, sticky="w", padx=5, pady=2)
def _on_select_all_managers_toggle(self):
@@ -654,8 +680,7 @@ class MaterialValidationTab(BaseTab):
# 管理员:从复选框获取选中的负责人
return [
manager for manager, var in self.manager_checkboxes.items()
if var.get()
manager for manager, var in self.manager_checkboxes.items() if var.get()
]
def _apply_manager_filter(self):
@@ -677,14 +702,19 @@ class MaterialValidationTab(BaseTab):
# 筛选记录:包含选中负责人的记录 + 负责人为空的记录
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() == "")
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() == ""
)
]
# 统计空负责人的记录数
empty_manager_count = sum(
1 for r in filtered_records
1
for r in filtered_records
if not r.manager_name or r.manager_name.strip() == ""
)
@@ -692,7 +722,7 @@ class MaterialValidationTab(BaseTab):
self._update_log(
f"筛选结果:共 {len(filtered_records)} 条记录"
f"(其中 {empty_manager_count} 条负责人为空,待编辑)",
"INFO"
"INFO",
)
def _refresh_filtered_results(self, filtered_records):
@@ -706,19 +736,22 @@ class MaterialValidationTab(BaseTab):
# 从数据库获取已标记删除的记录
from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO
record_dao = MaterialsToBeDeletedDAO()
# PERMISSION CHECK: 非管理员用户只获取自己的记录
if not self.session_manager.is_admin():
marked_records = record_dao.get_materials_by_manager(self.session_manager.get_username())
marked_records = record_dao.get_materials_by_manager(
self.session_manager.get_username()
)
else:
marked_records = record_dao.get_all_records()
# Build dictionary: MaterialCode -> ManagerName
marked_codes_dict = {
r['MaterialCode']: r['ManagerName']
r["MaterialCode"]: r["ManagerName"]
for r in marked_records
if r.get('MaterialCode') and r.get('ManagerName')
if r.get("MaterialCode") and r.get("ManagerName")
}
# 填充筛选后的记录
@@ -771,9 +804,7 @@ class MaterialValidationTab(BaseTab):
# 弹出编辑对话框
new_value = simpledialog.askstring(
"编辑负责人",
f"请输入负责人姓名:",
initialvalue=current_value
"编辑负责人", f"请输入负责人姓名:", initialvalue=current_value
)
if new_value is not None: # 用户没有取消
@@ -853,9 +884,14 @@ class MaterialValidationTab(BaseTab):
# User 模式:自动使用共享的 Production ID
production_ids_list = self.shared_production_ids
if not production_ids_list:
messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试")
messagebox.showerror(
"错误",
"没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试",
)
return
self.log_text.info(f"使用数据提取页面的 Production ID{len(production_ids_list)} 个)")
self.log_text.info(
f"使用数据提取页面的 Production ID{len(production_ids_list)} 个)"
)
else:
# Admin 模式:检查数据源选择
source = self.production_id_source_var.get()
@@ -863,9 +899,14 @@ class MaterialValidationTab(BaseTab):
# 使用共享的 Production ID
production_ids_list = self.shared_production_ids
if not production_ids_list:
messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试")
messagebox.showerror(
"错误",
"没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试",
)
return
self.log_text.info(f"使用共享的 Production ID{len(production_ids_list)} 个)")
self.log_text.info(
f"使用共享的 Production ID{len(production_ids_list)} 个)"
)
else:
# 使用文件
production_id_file = self.db_filtered_production_id_selector.get()
@@ -873,7 +914,9 @@ class MaterialValidationTab(BaseTab):
messagebox.showerror("错误", "请选择 ProductionID 文件")
return
if not os.path.exists(production_id_file):
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
messagebox.showerror(
"错误", f"文件不存在:{production_id_file}"
)
return
else:
messagebox.showerror("错误", f"未知的校验模式: {mode}")
@@ -897,9 +940,9 @@ class MaterialValidationTab(BaseTab):
# 禁用筛选按钮User 模式)
if not self.session_manager.is_admin():
if hasattr(self, 'btn_hide_checked'):
if hasattr(self, "btn_hide_checked"):
self.btn_hide_checked.config(state=tk.DISABLED)
if hasattr(self, 'btn_show_all'):
if hasattr(self, "btn_show_all"):
self.btn_show_all.config(state=tk.DISABLED)
# 保存当前筛选状态
@@ -914,26 +957,43 @@ class MaterialValidationTab(BaseTab):
# 在后台线程中执行校验
validation_thread = threading.Thread(
target=self._validation_worker_enhanced,
args=(mode, input_file, production_id_file, output_file, production_ids_list),
args=(
mode,
input_file,
production_id_file,
output_file,
production_ids_list,
),
daemon=True,
)
validation_thread.start()
def _validation_worker_enhanced(
self, mode: str, input_file: str, production_id_file: str, output_file: str, production_ids_list: list = None
self,
mode: str,
input_file: str,
production_id_file: str,
output_file: str,
production_ids_list: list = None,
):
"""增强的校验工作线程(使用完整记录模式)"""
import tempfile
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:
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))
f.write("\n".join(production_ids_list))
production_id_file = temp_production_id_file
self._update_log(f"使用共享的 Production ID 列表({len(production_ids_list)} 个)", "INFO")
self._update_log(
f"使用共享的 Production ID 列表({len(production_ids_list)} 个)",
"INFO",
)
# 导入校验器
from utils.material_status_validator import MaterialStatusValidator
@@ -953,14 +1013,13 @@ class MaterialValidationTab(BaseTab):
with redirect_stdout(captured_output):
if mode == "database_full":
result_file, results = validator.validate_from_database_enhanced(
full_table=True,
output_file=output_file
full_table=True, output_file=output_file
)
elif mode == "database_filtered":
result_file, results = validator.validate_from_database_enhanced(
production_id_file=production_id_file,
full_table=False,
output_file=output_file
output_file=output_file,
)
else:
raise ValueError(f"未知的校验模式: {mode}")
@@ -981,13 +1040,16 @@ class MaterialValidationTab(BaseTab):
self._load_results_with_deletion_status(result_file)
elif results is not None and len(results) == 0:
# 已经在 validator 中输出详细错误信息,这里只做简单提示
self._update_log("校验失败:未找到物料记录,请查看上方日志了解详细原因", "ERROR")
self._update_log(
"校验失败:未找到物料记录,请查看上方日志了解详细原因", "ERROR"
)
else:
self._update_log("校验失败", "ERROR")
except Exception as e:
self._update_log(f"校验过程中发生错误:{str(e)}", "ERROR")
import traceback
self._update_log(traceback.format_exc(), "ERROR")
finally:
# 清理临时文件
@@ -1009,21 +1071,24 @@ class MaterialValidationTab(BaseTab):
# 启用执行删除按钮(需要 Production ID
if self.shared_production_ids or (
self.session_manager.is_admin() and
hasattr(self, 'db_filtered_production_id_selector') and
self.db_filtered_production_id_selector.get()
self.session_manager.is_admin()
and hasattr(self, "db_filtered_production_id_selector")
and self.db_filtered_production_id_selector.get()
):
self.execute_delete_button.config(state=tk.NORMAL)
# 启用筛选按钮User 模式)
if not self.session_manager.is_admin():
if hasattr(self, 'btn_hide_checked'):
if hasattr(self, "btn_hide_checked"):
self.btn_hide_checked.config(state=tk.NORMAL)
if hasattr(self, 'btn_show_all'):
if hasattr(self, "btn_show_all"):
self.btn_show_all.config(state=tk.NORMAL)
# 恢复筛选状态
if hasattr(self, 'previously_selected_managers') and self.previously_selected_managers:
if (
hasattr(self, "previously_selected_managers")
and self.previously_selected_managers
):
self._restore_manager_filter_state(self.previously_selected_managers)
def _load_results_with_deletion_status(self, file_path: str):
@@ -1034,20 +1099,25 @@ class MaterialValidationTab(BaseTab):
# 在主线程中更新表格
def update_table():
# 从数据库获取已标记删除的 MaterialCode -> ManagerName 映射
from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO
from db.materials_to_be_deleted_records_dao import (
MaterialsToBeDeletedDAO,
)
record_dao = MaterialsToBeDeletedDAO()
# PERMISSION CHECK: 非管理员用户只获取自己的记录
if not self.session_manager.is_admin():
marked_records = record_dao.get_materials_by_manager(self.session_manager.get_username())
marked_records = record_dao.get_materials_by_manager(
self.session_manager.get_username()
)
else:
marked_records = record_dao.get_all_records()
# Build dictionary: MaterialCode -> ManagerName
marked_codes_dict = {
r['MaterialCode']: r['ManagerName']
r["MaterialCode"]: r["ManagerName"]
for r in marked_records
if r.get('MaterialCode') and r.get('ManagerName')
if r.get("MaterialCode") and r.get("ManagerName")
}
for _, row in df.iterrows():
@@ -1080,7 +1150,10 @@ class MaterialValidationTab(BaseTab):
if len(df) > 0:
self.export_button.config(state=tk.NORMAL)
self._update_log(f"已加载 {len(df)} 条结果(其中 {len(marked_codes_dict)} 条已标记删除)", "INFO")
self._update_log(
f"已加载 {len(df)} 条结果(其中 {len(marked_codes_dict)} 条已标记删除)",
"INFO",
)
# 初始化负责人筛选器
self._initialize_manager_filter()
@@ -1129,8 +1202,8 @@ class MaterialValidationTab(BaseTab):
return
# 收集所有数据
to_upsert = [] # 需要写入/更新的记录
to_delete = [] # 需要删除的记录
to_upsert = [] # 需要写入/更新的记录
to_delete = [] # 需要删除的记录
missing_manager = [] # 缺少负责人的记录
for item in self.tree.get_children():
@@ -1150,18 +1223,21 @@ class MaterialValidationTab(BaseTab):
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()
})
to_upsert.append(
{
"material_code": material_code,
"manager_name": manager_name.strip(),
}
)
else:
# 未勾选:需要删除
to_delete.append(material_code)
# 验证:已勾选的记录必须有负责人
if missing_manager:
msg = f"以下已勾选的记录缺少负责人信息,无法保存:\n\n" + \
"\n".join(missing_manager[:10])
msg = f"以下已勾选的记录缺少负责人信息,无法保存:\n\n" + "\n".join(
missing_manager[:10]
)
if len(missing_manager) > 10:
msg += f"\n... 共 {len(missing_manager)}"
messagebox.showwarning("警告", msg)
@@ -1183,7 +1259,10 @@ class MaterialValidationTab(BaseTab):
return
# 在后台线程中执行
self._update_log(f"开始处理:写入/更新 {len(to_upsert)} 条,删除 {len(to_delete)} 条...", "INFO")
self._update_log(
f"开始处理:写入/更新 {len(to_upsert)} 条,删除 {len(to_delete)} 条...",
"INFO",
)
deletion_thread = threading.Thread(
target=self._execute_sync_in_background,
@@ -1205,7 +1284,7 @@ class MaterialValidationTab(BaseTab):
dao = MaterialsToBeDeletedDAO()
# 执行写入/更新操作
upsert_stats = {'total': 0, 'success': 0, 'failed': 0}
upsert_stats = {"total": 0, "success": 0, "failed": 0}
if to_upsert:
upsert_stats = dao.upsert_batch(to_upsert)
@@ -1216,9 +1295,9 @@ class MaterialValidationTab(BaseTab):
# 组合统计信息
combined_stats = {
'upsert_success': upsert_stats['success'],
'upsert_failed': upsert_stats['failed'],
'deleted': delete_count
"upsert_success": upsert_stats["success"],
"upsert_failed": upsert_stats["failed"],
"deleted": delete_count,
}
self.after(0, lambda: self._sync_complete(combined_stats))
@@ -1232,9 +1311,9 @@ class MaterialValidationTab(BaseTab):
Args:
stats: 包含 upsert_success, upsert_failed, deleted 的字典
"""
upsert_success = stats.get('upsert_success', 0)
upsert_failed = stats.get('upsert_failed', 0)
deleted = stats.get('deleted', 0)
upsert_success = stats.get("upsert_success", 0)
upsert_failed = stats.get("upsert_failed", 0)
deleted = stats.get("deleted", 0)
msg_parts = []
if upsert_success > 0:
@@ -1260,14 +1339,15 @@ class MaterialValidationTab(BaseTab):
"""导出结果到 Excel"""
# 从配置获取输出文件路径
data_dir = self.config.get("paths.data_dir", "data/")
validation_filename = self.config.get("paths.validation_output", "物料状态校验结果.xlsx")
validation_filename = self.config.get(
"paths.validation_output", "物料状态校验结果.xlsx"
)
output_file = os.path.join(data_dir, validation_filename)
# 如果配置的文件不存在,提示用户选择位置
if not os.path.exists(data_dir):
output_dir = filedialog.askdirectory(
title="选择输出目录",
initialdir=data_dir
title="选择输出目录", initialdir=data_dir
)
if output_dir:
self.config.set("paths.data_dir", output_dir)
@@ -1279,7 +1359,9 @@ class MaterialValidationTab(BaseTab):
# 如果文件已存在,提示用户覆盖
if os.path.exists(output_file):
if not messagebox.askyesno("确认覆盖", f"文件已存在:{output_file}\n是否覆盖?"):
if not messagebox.askyesno(
"确认覆盖", f"文件已存在:{output_file}\n是否覆盖?"
):
return
try:
@@ -1291,14 +1373,16 @@ class MaterialValidationTab(BaseTab):
checkbox = values[0] if values else ""
is_checked = checkbox == ""
data.append([
"" if is_checked else "", # 选择状态
values[1] if len(values) > 1 else "", # 材料名称
values[2] if len(values) > 2 else "", # 材料代码
values[3] if len(values) > 3 else "", # 规格
values[4] if len(values) > 4 else "", # 型号
values[5] if len(values) > 5 else "", # 负责人
])
data.append(
[
"" if is_checked else "", # 选择状态
values[1] if len(values) > 1 else "", # 材料名称
values[2] if len(values) > 2 else "", # 材料代码
values[3] if len(values) > 3 else "", # 规格
values[4] if len(values) > 4 else "", # 型号
values[5] if len(values) > 5 else "", # 负责人
]
)
if not data:
messagebox.showwarning("警告", "没有数据可导出")
@@ -1319,7 +1403,9 @@ class MaterialValidationTab(BaseTab):
def open_type_management(self):
"""打开类型管理窗口"""
dialog = MaterialTypeManagementDialog(self, self.session_manager, title="物料类型管理")
dialog = MaterialTypeManagementDialog(
self, self.session_manager, title="物料类型管理"
)
def start_delete_execution(self):
"""开始执行删除"""
@@ -1329,7 +1415,7 @@ class MaterialValidationTab(BaseTab):
if is_admin:
# Admin 用户:检查数据源选择
if hasattr(self, 'production_id_source_var'):
if hasattr(self, "production_id_source_var"):
source = self.production_id_source_var.get()
if source == "shared":
production_ids = self.shared_production_ids
@@ -1337,6 +1423,7 @@ class MaterialValidationTab(BaseTab):
production_id_file = self.db_filtered_production_id_selector.get()
if production_id_file and os.path.exists(production_id_file):
from db.production_order_query import read_production_ids
production_ids = read_production_ids(production_id_file)
else:
# 普通用户:使用共享的 Production ID
@@ -1344,7 +1431,9 @@ class MaterialValidationTab(BaseTab):
# 验证 Production ID
if not production_ids:
messagebox.showerror("错误", "没有可用的 Production ID\n请先在校验页面获取数据")
messagebox.showerror(
"错误", "没有可用的 Production ID\n请先在校验页面获取数据"
)
return
# 2. 获取负责人
@@ -1358,7 +1447,7 @@ class MaterialValidationTab(BaseTab):
# 3. 获取 dryrun 设置
dryrun = False
if is_admin and hasattr(self, 'dryrun_var'):
if is_admin and hasattr(self, "dryrun_var"):
# Admin 用户:使用界面上的 dryrun 复选框
dryrun = self.dryrun_var.get()
else:
@@ -1384,7 +1473,7 @@ class MaterialValidationTab(BaseTab):
title="执行删除",
managers=manager_text,
dryrun=dryrun,
on_cancel=self._cancel_delete_execution
on_cancel=self._cancel_delete_execution,
)
self.log_text.info(f"开始执行删除(模式: {'预览' if dryrun else '正式'}...")
@@ -1393,14 +1482,14 @@ class MaterialValidationTab(BaseTab):
delete_thread = threading.Thread(
target=self._delete_worker,
args=(production_ids, manager_names, dryrun),
daemon=True
daemon=True,
)
delete_thread.start()
def _cancel_delete_execution(self):
"""取消删除执行"""
self.log_text.info("用户取消了执行操作")
if hasattr(self, 'progress_window') and self.progress_window:
if hasattr(self, "progress_window") and self.progress_window:
self.progress_window.append_log("正在取消...", "warning")
def _delete_worker(self, production_ids: list, manager_names: list, dryrun: bool):
@@ -1420,9 +1509,11 @@ class MaterialValidationTab(BaseTab):
from db.production_order_query import query_production_order_numbers
# 创建临时文件保存 Production ID
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
temp_file = f.name
f.write('\n'.join(production_ids))
f.write("\n".join(production_ids))
# 进度回调函数
def progress_callback(current: int, total: int, message: str):
@@ -1439,7 +1530,7 @@ class MaterialValidationTab(BaseTab):
headless=self.config.get("erp.headless", True),
verbose=True,
dryrun=dryrun,
progress_callback=progress_callback
progress_callback=progress_callback,
)
# 执行清理
@@ -1464,12 +1555,13 @@ class MaterialValidationTab(BaseTab):
progress_window.append_log(error_msg, "error")
progress_window.set_completed()
import traceback
self.after(0, lambda: self._update_log(traceback.format_exc(), "ERROR"))
finally:
# 清理临时文件
try:
if 'temp_file' in locals() and os.path.exists(temp_file):
if "temp_file" in locals() and os.path.exists(temp_file):
os.unlink(temp_file)
except OSError as e:
self.logger.debug(f"清理临时文件失败: {e}")
@@ -1484,11 +1576,13 @@ class MaterialValidationTab(BaseTab):
"""
# 更新日志
self.log_text.info("执行完成!")
self.log_text.info(f"处理订单: {stats.get('processed_orders', 0)}/{stats.get('total_orders', 0)}")
self.log_text.info(
f"处理订单: {stats.get('processed_orders', 0)}/{stats.get('total_orders', 0)}"
)
self.log_text.info(f"删除物料: {len(stats.get('deleted_materials', []))}")
self.log_text.info(f"跳过物料: {len(stats.get('skipped_materials', []))}")
self.log_text.info(f"错误数量: {len(stats.get('errors', []))}")
# 显示报告
if hasattr(self, 'progress_window') and self.progress_window:
if hasattr(self, "progress_window") and self.progress_window:
self.progress_window.show_report(report)

View File

@@ -18,9 +18,7 @@ class ProgressInfo:
用于在后台任务和 GUI 之间传递进度信息。
"""
stage: (
str # 阶段标识: 'login', 'query', 'download', 'logout', 'convert', 'database', 'complete'
)
stage: str # 阶段标识: 'login', 'query', 'download', 'logout', 'convert', 'database', 'complete'
current: int # 当前进度值
total: int # 总量
message: str # 显示给用户的消息

View File

@@ -38,7 +38,9 @@ class SettingsTab(ttk.Frame):
def create_widgets(self):
"""创建界面组件"""
# 判断用户类型
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
is_user_only = (
self.session_manager and self.session_manager.get_user_type() == "User"
)
# 创建主容器,带滚动条
canvas = tk.Canvas(self)
@@ -158,7 +160,9 @@ class SettingsTab(ttk.Frame):
self.sqlserver_frame = ttk.Frame(group)
self.sqlserver_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
ttk.Label(self.sqlserver_frame, text="服务器:").grid(row=0, column=0, sticky="w", pady=5)
ttk.Label(self.sqlserver_frame, text="服务器:").grid(
row=0, column=0, sticky="w", pady=5
)
self.db_server_var = tk.StringVar()
ttk.Entry(self.sqlserver_frame, textvariable=self.db_server_var, width=50).grid(
row=0, column=1, pady=5, sticky="ew"
@@ -167,16 +171,24 @@ class SettingsTab(ttk.Frame):
# MySQL 配置
self.mysql_frame = ttk.Frame(group)
ttk.Label(self.mysql_frame, text="主机:").grid(row=0, column=0, sticky="w", pady=5)
ttk.Label(self.mysql_frame, text="主机:").grid(
row=0, column=0, sticky="w", pady=5
)
self.mysql_host_var = tk.StringVar()
ttk.Entry(self.mysql_frame, textvariable=self.mysql_host_var, width=50).grid(
row=0, column=1, pady=5, sticky="ew"
)
ttk.Label(self.mysql_frame, text="端口:").grid(row=1, column=0, sticky="w", pady=5)
ttk.Label(self.mysql_frame, text="端口:").grid(
row=1, column=0, sticky="w", pady=5
)
self.mysql_port_var = tk.IntVar(value=3306)
ttk.Spinbox(
self.mysql_frame, from_=1, to=65535, textvariable=self.mysql_port_var, width=10
self.mysql_frame,
from_=1,
to=65535,
textvariable=self.mysql_port_var,
width=10,
).grid(row=1, column=1, sticky="w", pady=5)
# 通用配置(两种数据库都需要)
@@ -207,7 +219,9 @@ class SettingsTab(ttk.Frame):
self.mysql_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
else:
self.mysql_frame.grid_forget()
self.sqlserver_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
self.sqlserver_frame.grid(
row=1, column=0, columnspan=2, sticky="ew", pady=5
)
def _create_browser_group(self, parent):
"""创建浏览器配置组"""
@@ -232,7 +246,9 @@ class SettingsTab(ttk.Frame):
def _create_paths_group(self, parent):
"""创建路径配置组"""
# 根据用户类型调整 grid 位置
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
is_user_only = (
self.session_manager and self.session_manager.get_user_type() == "User"
)
group = ttk.LabelFrame(parent, text="路径设置", padding=10)
if is_user_only:
@@ -261,9 +277,9 @@ class SettingsTab(ttk.Frame):
# 校验输出文件
ttk.Label(group, text="校验输出文件:").grid(row=3, column=0, sticky="w", pady=5)
ttk.Entry(group, textvariable=self.validation_output_filename_var, width=40).grid(
row=3, column=1, columnspan=2, sticky="ew", pady=5
)
ttk.Entry(
group, textvariable=self.validation_output_filename_var, width=40
).grid(row=3, column=1, columnspan=2, sticky="ew", pady=5)
group.columnconfigure(0, weight=1)
@@ -275,16 +291,14 @@ class SettingsTab(ttk.Frame):
# dryrun 模式设置
self.user_dryrun_var = tk.BooleanVar(value=False)
ttk.Checkbutton(
group,
text="预览模式 (执行删除时不保存更改)",
variable=self.user_dryrun_var
group, text="预览模式 (执行删除时不保存更改)", variable=self.user_dryrun_var
).grid(row=0, column=0, sticky="w", pady=5)
# 说明文字
hint_label = ttk.Label(
group,
text="提示:勾选后,执行删除操作时将只预览不实际保存,用于测试流程。",
foreground="gray"
foreground="gray",
)
hint_label.grid(row=1, column=0, sticky="w", pady=(0, 5))
@@ -335,7 +349,12 @@ class SettingsTab(ttk.Frame):
data_source_combo = ttk.Combobox(
group,
textvariable=self.validation_data_source_var,
values=["database_full", "database_filtered", "excel_existing", "excel_full"],
values=[
"database_full",
"database_filtered",
"excel_existing",
"excel_full",
],
state="readonly",
width=30,
)
@@ -344,20 +363,28 @@ class SettingsTab(ttk.Frame):
# 使用数据库
self.validation_use_database_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="使用数据库作为数据源", variable=self.validation_use_database_var
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)
ttk.Entry(group, textvariable=self.validation_output_filename_var, width=30).grid(
row=2, column=1, sticky="w", pady=5
)
ttk.Entry(
group, textvariable=self.validation_output_filename_var, width=30
).grid(row=2, column=1, sticky="w", pady=5)
# 批次大小
ttk.Label(group, text="数据库批次大小:").grid(row=3, column=0, sticky="w", pady=5)
ttk.Label(group, text="数据库批次大小:").grid(
row=3, 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
group,
from_=100,
to=2000,
textvariable=self.validation_batch_size_var,
width=10,
).grid(row=3, column=1, sticky="w", pady=5)
# 匹配模式
@@ -375,15 +402,17 @@ class SettingsTab(ttk.Frame):
# CRUD 操作
self.validation_enable_crud_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="启用 CRUD 操作(管理待删除物料)", variable=self.validation_enable_crud_var
group,
text="启用 CRUD 操作(管理待删除物料)",
variable=self.validation_enable_crud_var,
).grid(row=5, column=0, columnspan=2, sticky="w", pady=5)
# 默认负责人
ttk.Label(group, text="默认负责人:").grid(row=6, 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=6, column=1, sticky="w", pady=5
)
ttk.Entry(
group, textvariable=self.validation_default_manager_var, width=30
).grid(row=6, column=1, sticky="w", pady=5)
group.columnconfigure(1, weight=1)
@@ -398,7 +427,14 @@ class SettingsTab(ttk.Frame):
font_combo = ttk.Combobox(
group,
textvariable=self.ui_font_family_var,
values=["Microsoft YaHei UI", "SimSun", "KaiTi", "FangSong", "Arial", "Segoe UI"],
values=[
"Microsoft YaHei UI",
"SimSun",
"KaiTi",
"FangSong",
"Arial",
"Segoe UI",
],
state="readonly",
width=30,
)
@@ -412,7 +448,9 @@ class SettingsTab(ttk.Frame):
).grid(row=1, column=1, sticky="w", pady=5)
# Production ID 输入框宽度
ttk.Label(group, text="输入框宽度(字符):").grid(row=2, column=0, sticky="w", pady=5)
ttk.Label(group, text="输入框宽度(字符):").grid(
row=2, column=0, sticky="w", pady=5
)
self.ui_input_width_var = tk.IntVar(value=20)
ttk.Spinbox(
group, from_=10, to=100, textvariable=self.ui_input_width_var, width=10
@@ -423,14 +461,18 @@ class SettingsTab(ttk.Frame):
def load_settings(self):
"""从配置加载设置到界面"""
# 判断是否为仅测试用户模式
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
is_user_only = (
self.session_manager and self.session_manager.get_user_type() == "User"
)
if is_user_only:
# User 用户模式 - 只需要加载路径设置到界面
# 路径设置
self.data_dir_selector.set(self.config.get("paths.data_dir", ""))
self.default_output_var.set(self.config.get("paths.default_output", ""))
self.validation_output_filename_var.set(self.config.get("paths.validation_output", ""))
self.validation_output_filename_var.set(
self.config.get("paths.validation_output", "")
)
# 执行设置
self.user_dryrun_var.set(self.config.get("execution.dryrun", False))
return
@@ -475,32 +517,54 @@ class SettingsTab(ttk.Frame):
self.verbose_var.set(self.config.get("extraction.verbose", True))
self.auto_convert_var.set(self.config.get("extraction.auto_convert", True))
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.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_output_filename_var.set(self.config.get("paths.validation_output", "物料状态校验结果.xlsx"))
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", ""))
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_output_filename_var.set(
self.config.get("paths.validation_output", "物料状态校验结果.xlsx")
)
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", "")
)
# UI 设置
self.ui_font_family_var.set(self.config.get("ui.font_family", "Microsoft YaHei UI"))
self.ui_font_family_var.set(
self.config.get("ui.font_family", "Microsoft YaHei UI")
)
self.ui_font_size_var.set(self.config.get("ui.font_size", 10))
self.ui_input_width_var.set(self.config.get("ui.production_id_input_width", 20))
def save_settings(self):
"""保存界面设置到配置"""
# 判断是否为仅测试用户模式
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
is_user_only = (
self.session_manager and self.session_manager.get_user_type() == "User"
)
if is_user_only:
# User 用户模式 - 只保存路径设置和执行设置
self.config.set("paths.data_dir", self.data_dir_selector.get())
self.config.set("paths.default_output", self.default_output_var.get())
self.config.set("paths.validation_output", self.validation_output_filename_var.get())
self.config.set(
"paths.validation_output", self.validation_output_filename_var.get()
)
# 保存执行设置
self.config.set("execution.dryrun", self.user_dryrun_var.get())
@@ -550,16 +614,26 @@ class SettingsTab(ttk.Frame):
self.config.set("extraction.verbose", self.verbose_var.get())
self.config.set("extraction.auto_convert", self.auto_convert_var.get())
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(
"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("paths.validation_output", self.validation_output_filename_var.get())
self.config.set(
"validation.use_database", self.validation_use_database_var.get()
)
self.config.set(
"paths.validation_output", self.validation_output_filename_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())
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()
)
# UI 设置
self.config.set("ui.font_family", self.ui_font_family_var.get())
@@ -581,7 +655,7 @@ class SettingsTab(ttk.Frame):
# 获取主窗口
main_window = self.winfo_toplevel()
# 调用主窗口的 reload_config 方法(如果存在)
if hasattr(main_window, 'reload_config'):
if hasattr(main_window, "reload_config"):
main_window.reload_config()
except Exception:
pass
@@ -602,7 +676,7 @@ class SettingsTab(ttk.Frame):
database=self.config.get("database.database", ""),
user=self.config.get("database.username", ""),
password=self.config.get("database.password", ""),
connection_timeout=5
connection_timeout=5,
)
conn.close()
messagebox.showinfo("成功", "MySQL 数据库连接测试成功!")
@@ -621,9 +695,14 @@ class SettingsTab(ttk.Frame):
except ImportError:
if db_type == "mysql":
messagebox.showerror("错误", "未安装 mysql-connector-python请运行\npip install mysql-connector-python")
messagebox.showerror(
"错误",
"未安装 mysql-connector-python请运行\npip install mysql-connector-python",
)
else:
messagebox.showerror("错误", "未安装 pyodbc请运行\npip install pyodbc")
messagebox.showerror(
"错误", "未安装 pyodbc请运行\npip install pyodbc"
)
except Exception as e:
messagebox.showerror("错误", f"数据库连接失败:\n{str(e)}")
@@ -634,8 +713,11 @@ class SettingsTab(ttk.Frame):
def reset_defaults(self):
"""恢复默认设置"""
if messagebox.askyesno("确认", "确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。"):
if messagebox.askyesno(
"确认", "确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。"
):
from config.schema import AppConfig
self.config.config = AppConfig.from_env() # 重新加载默认配置
self.config.save()
self.load_settings()

View File

@@ -1,6 +1,7 @@
"""
User Selection Dialog - Allows Admin to choose which user identity to use
"""
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Optional, Dict, Any, List
@@ -64,9 +65,7 @@ class UserSelectionDialog:
# Title
title_label = ttk.Label(
main_frame,
text="请选择要使用的用户身份",
font=('', 14, 'bold')
main_frame, text="请选择要使用的用户身份", font=("", 14, "bold")
)
title_label.pack(pady=(0, 20))
@@ -82,12 +81,15 @@ class UserSelectionDialog:
# Sort users: current user first, then by username
sorted_users = sorted(
self.users,
key=lambda u: (0 if u['username'] == self.current_username else 1, u['username'])
key=lambda u: (
0 if u["username"] == self.current_username else 1,
u["username"],
),
)
for user in sorted_users:
username = user['username']
user_type = user['user_type']
username = user["username"]
user_type = user["user_type"]
is_current = username == self.current_username
# Mark current user
@@ -99,7 +101,7 @@ class UserSelectionDialog:
list_frame,
text=display_text,
variable=self.selected_var,
value=username
value=username,
)
rb.pack(anchor=tk.W, pady=3, padx=5)
@@ -111,18 +113,12 @@ class UserSelectionDialog:
button_frame.pack(pady=(20, 0))
confirm_btn = ttk.Button(
button_frame,
text="确认",
command=self._on_confirm,
width=10
button_frame, text="确认", command=self._on_confirm, width=10
)
confirm_btn.pack(side=tk.LEFT, padx=5)
cancel_btn = ttk.Button(
button_frame,
text="取消",
command=self._on_cancel,
width=10
button_frame, text="取消", command=self._on_cancel, width=10
)
cancel_btn.pack(side=tk.LEFT, padx=5)
@@ -136,7 +132,7 @@ class UserSelectionDialog:
# Find the selected user
for user in self.users:
if user['username'] == selected_username:
if user["username"] == selected_username:
self.selected_user = user
break

View File

@@ -61,24 +61,25 @@ def admin_only(func):
Returns:
如果是管理员,返回原函数的结果;否则返回 None
"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# 尝试从 self 获取 session_manager
session_manager = getattr(self, 'session_manager', None)
session_manager = getattr(self, "session_manager", None)
# 如果没有 session_manager尝试从 main_window 获取
if session_manager is None:
main_window = getattr(self, 'main_window', None)
main_window = getattr(self, "main_window", None)
if main_window:
session_manager = getattr(main_window, 'session_manager', None)
session_manager = getattr(main_window, "session_manager", None)
# 检查是否为管理员
if session_manager and hasattr(session_manager, 'is_admin'):
if session_manager and hasattr(session_manager, "is_admin"):
if session_manager.is_admin():
return func(self, *args, **kwargs)
# 非管理员,记录日志并返回 None
logger = getattr(self, 'logger', None)
logger = getattr(self, "logger", None)
if logger:
logger.debug(f"权限拒绝: {func.__name__} 需要管理员权限")
@@ -103,24 +104,25 @@ def require_session(func):
Returns:
如果有有效会话,返回原函数的结果;否则返回 None
"""
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# 尝试从 self 获取 session_manager
session_manager = getattr(self, 'session_manager', None)
session_manager = getattr(self, "session_manager", None)
# 如果没有 session_manager尝试从 main_window 获取
if session_manager is None:
main_window = getattr(self, 'main_window', None)
main_window = getattr(self, "main_window", None)
if main_window:
session_manager = getattr(main_window, 'session_manager', None)
session_manager = getattr(main_window, "session_manager", None)
# 检查会话是否有效
if session_manager and hasattr(session_manager, 'is_authenticated'):
if session_manager and hasattr(session_manager, "is_authenticated"):
if session_manager.is_authenticated():
return func(self, *args, **kwargs)
# 会话无效,记录日志并返回 None
logger = getattr(self, 'logger', None)
logger = getattr(self, "logger", None)
if logger:
logger.warning(f"会话无效: {func.__name__} 需要有效会话")

View File

@@ -12,10 +12,10 @@ from .delete_progress_window import DeleteProgressWindow
from .checkbox_treeview import CheckboxTreeview
__all__ = [
'FileSelector',
'LogText',
'ProductionIdInput',
'GuiTextHandler',
'DeleteProgressWindow',
'CheckboxTreeview'
"FileSelector",
"LogText",
"ProductionIdInput",
"GuiTextHandler",
"DeleteProgressWindow",
"CheckboxTreeview",
]

View File

@@ -39,10 +39,10 @@ class CheckboxTreeview(ttk.Treeview):
self.on_checkbox_change = on_checkbox_change # checkbox 状态改变回调
# 排序状态
self.sort_column = None # 当前排序列的列标识符
self.sort_direction = None # 'asc', 'desc', 或 None
self.sort_column = None # 当前排序列的列标识符
self.sort_direction = None # 'asc', 'desc', 或 None
self.sortable_columns = ["选择", "材料名称"] # 可排序的列白名单
self.original_headings = {} # 存储原始列标题文本(不含箭头)
self.original_headings = {} # 存储原始列标题文本(不含箭头)
# 存储原始列标题(延迟执行以确保标题已设置)
self.after(100, self._store_original_headings)
@@ -121,7 +121,7 @@ class CheckboxTreeview(ttk.Treeview):
# 初始化 checkbox 状态为未选中
checkbox_char = values[0] if values else ""
self.checkboxes[item] = (checkbox_char == "")
self.checkboxes[item] = checkbox_char == ""
return item
@@ -134,8 +134,8 @@ class CheckboxTreeview(ttk.Treeview):
def _store_original_headings(self):
"""存储原始列标题文本(不含箭头)"""
for col in self['columns']:
self.original_headings[col] = self.heading(col, 'text')
for col in self["columns"]:
self.original_headings[col] = self.heading(col, "text")
def _get_column_id_from_column_index(self, column_index):
"""将列索引 ('#1', '#2') 转换为列标识符
@@ -147,7 +147,7 @@ class CheckboxTreeview(ttk.Treeview):
列标识符,如 '选择', '材料名称'
"""
index = int(column_index[1:]) - 1
columns = self['columns']
columns = self["columns"]
if 0 <= index < len(columns):
return columns[index]
return None
@@ -173,15 +173,15 @@ class CheckboxTreeview(ttk.Treeview):
# 确定新的排序方向
if self.sort_column == column_id:
# 同一列asc -> desc -> None
if self.sort_direction == 'asc':
new_direction = 'desc'
elif self.sort_direction == 'desc':
if self.sort_direction == "asc":
new_direction = "desc"
elif self.sort_direction == "desc":
new_direction = None
else:
new_direction = 'asc'
new_direction = "asc"
else:
# 不同列:从升序开始
new_direction = 'asc'
new_direction = "asc"
# 应用排序
if new_direction:
@@ -208,35 +208,33 @@ class CheckboxTreeview(ttk.Treeview):
for item in self.get_children():
values = self.item(item, "values")
checkbox_state = self.checkboxes.get(item, False)
items_data.append({
'item_id': item,
'values': values,
'checked': checkbox_state
})
items_data.append(
{"item_id": item, "values": values, "checked": checkbox_state}
)
# 根据列和方向排序
if column_id == "选择":
# 按复选框状态排序(选中在前,未选中在后)
items_data.sort(key=lambda x: x['checked'], reverse=(direction == 'desc'))
items_data.sort(key=lambda x: x["checked"], reverse=(direction == "desc"))
elif column_id == "材料名称":
# 按材料名称排序
items_data.sort(
key=lambda x: str(x['values'][1]) if len(x['values']) > 1 else "",
reverse=(direction == 'desc')
key=lambda x: str(x["values"][1]) if len(x["values"]) > 1 else "",
reverse=(direction == "desc"),
)
# 重新排列项目顺序(使用 detach 和 move 保留项目ID和状态
for item_data in items_data:
self.move(item_data['item_id'], '', 'end')
self.move(item_data["item_id"], "", "end")
def _update_heading_display(self):
"""更新列标题显示(添加/移除排序箭头)"""
for col in self['columns']:
for col in self["columns"]:
original = self.original_headings.get(col, col)
if col == self.sort_column:
# 添加排序箭头
arrow = "" if self.sort_direction == 'asc' else ""
arrow = "" if self.sort_direction == "asc" else ""
self.heading(col, text=original + arrow)
else:
# 移除箭头,显示原始标题
self.heading(col, text=original)
self.heading(col, text=original)

View File

@@ -14,12 +14,14 @@ from datetime import datetime
# 尝试导入 tkinterweb 和 markdown2
try:
from tkinterweb import HtmlFrame
HAS_TKINTERWEB = True
except ImportError:
HAS_TKINTERWEB = False
try:
import markdown2
HAS_MARKDOWN2 = True
except ImportError:
HAS_MARKDOWN2 = False
@@ -34,7 +36,7 @@ class DeleteProgressWindow:
title: str = "执行删除",
managers: str = "",
dryrun: bool = False,
on_cancel: Optional[Callable] = None
on_cancel: Optional[Callable] = None,
):
"""
初始化删除进度窗口
@@ -100,14 +102,13 @@ class DeleteProgressWindow:
self.progress_frame.pack(fill=tk.X, pady=(0, 10))
self.progress_var = tk.StringVar(value="准备中...")
self.progress_label = ttk.Label(self.progress_frame, textvariable=self.progress_var)
self.progress_label = ttk.Label(
self.progress_frame, textvariable=self.progress_var
)
self.progress_label.pack(anchor="w")
self.progress_bar = ttk.Progressbar(
self.progress_frame,
mode='determinate',
length=660,
maximum=100
self.progress_frame, mode="determinate", length=660, maximum=100
)
self.progress_bar.pack(fill=tk.X, pady=5)
@@ -120,15 +121,15 @@ class DeleteProgressWindow:
height=10,
wrap=tk.WORD,
state=tk.DISABLED,
font=('Consolas', 9)
font=("Consolas", 9),
)
self.log_text.pack(fill=tk.BOTH, expand=True)
# 配置日志标签颜色
self.log_text.tag_configure('info', foreground='black')
self.log_text.tag_configure('success', foreground='green')
self.log_text.tag_configure('warning', foreground='orange')
self.log_text.tag_configure('error', foreground='red')
self.log_text.tag_configure("info", foreground="black")
self.log_text.tag_configure("success", foreground="green")
self.log_text.tag_configure("warning", foreground="orange")
self.log_text.tag_configure("error", foreground="red")
# 报告区域(完成后显示)- 初始隐藏
self.report_frame = ttk.LabelFrame(self.main_frame, text="执行报告", padding=5)
@@ -145,7 +146,7 @@ class DeleteProgressWindow:
height=20,
wrap=tk.WORD,
state=tk.DISABLED,
font=('Consolas', 9)
font=("Consolas", 9),
)
self.report_text.pack(fill=tk.BOTH, expand=True)
@@ -154,18 +155,12 @@ class DeleteProgressWindow:
button_frame.pack(fill=tk.X)
self.cancel_button = ttk.Button(
button_frame,
text="取消执行",
command=self._on_cancel
button_frame, text="取消执行", command=self._on_cancel
)
self.cancel_button.pack(side=tk.RIGHT)
# 关闭按钮(初始隐藏)
self.close_button = ttk.Button(
button_frame,
text="关闭",
command=self.close
)
self.close_button = ttk.Button(button_frame, text="关闭", command=self.close)
def _on_cancel(self):
"""处理取消操作"""
@@ -187,7 +182,7 @@ class DeleteProgressWindow:
"""
if total > 0:
percentage = int((current / total) * 100)
self.progress_bar['value'] = percentage
self.progress_bar["value"] = percentage
self.progress_var.set(message)
else:
self.progress_var.set(message)
@@ -263,8 +258,7 @@ class DeleteProgressWindow:
"""
# 使用 markdown2 转换
html_body = markdown2.markdown(
markdown_content,
extras=['tables', 'fenced-code-blocks']
markdown_content, extras=["tables", "fenced-code-blocks"]
)
# 添加样式
@@ -340,59 +334,65 @@ class DeleteProgressWindow:
Returns:
HTML 内容
"""
lines = markdown_content.split('\n')
html_parts = ['<!DOCTYPE html><html><head><meta charset="UTF-8">',
'<style>',
'body { font-family: "Microsoft YaHei", Arial, sans-serif; font-size: 12px; padding: 10px; }',
'h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }',
'h2 { color: #34495e; border-bottom: 1px solid #bdc3c7; margin-top: 20px; }',
'table { border-collapse: collapse; width: 100%; margin: 10px 0; }',
'th, td { border: 1px solid #bdc3c7; padding: 8px; text-align: left; }',
'th { background-color: #3498db; color: white; }',
'</style></head><body>']
lines = markdown_content.split("\n")
html_parts = [
'<!DOCTYPE html><html><head><meta charset="UTF-8">',
"<style>",
'body { font-family: "Microsoft YaHei", Arial, sans-serif; font-size: 12px; padding: 10px; }',
"h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }",
"h2 { color: #34495e; border-bottom: 1px solid #bdc3c7; margin-top: 20px; }",
"table { border-collapse: collapse; width: 100%; margin: 10px 0; }",
"th, td { border: 1px solid #bdc3c7; padding: 8px; text-align: left; }",
"th { background-color: #3498db; color: white; }",
"</style></head><body>",
]
in_table = False
for line in lines:
if line.startswith('# '):
html_parts.append(f'<h1>{line[2:]}</h1>')
elif line.startswith('## '):
html_parts.append(f'<h2>{line[3:]}</h2>')
elif line.startswith('| '):
if line.startswith("# "):
html_parts.append(f"<h1>{line[2:]}</h1>")
elif line.startswith("## "):
html_parts.append(f"<h2>{line[3:]}</h2>")
elif line.startswith("| "):
if not in_table:
html_parts.append('<table>')
html_parts.append("<table>")
in_table = True
# 检查是否是表头分隔行
if '|--' in line or '|-' in line:
if "|--" in line or "|-" in line:
continue
cells = [cell.strip() for cell in line.split('|')[1:-1]]
cells = [cell.strip() for cell in line.split("|")[1:-1]]
if cells:
# 第一行作为表头
if html_parts[-1] == '<table>':
html_parts.append('<tr>' + ''.join(f'<th>{c}</th>' for c in cells) + '</tr>')
if html_parts[-1] == "<table>":
html_parts.append(
"<tr>" + "".join(f"<th>{c}</th>" for c in cells) + "</tr>"
)
else:
html_parts.append('<tr>' + ''.join(f'<td>{c}</td>' for c in cells) + '</tr>')
elif line.startswith('- '):
html_parts.append(
"<tr>" + "".join(f"<td>{c}</td>" for c in cells) + "</tr>"
)
elif line.startswith("- "):
if in_table:
html_parts.append('</table>')
html_parts.append("</table>")
in_table = False
html_parts.append(f'<li>{line[2:]}</li>')
elif line.strip() == '':
html_parts.append(f"<li>{line[2:]}</li>")
elif line.strip() == "":
if in_table:
html_parts.append('</table>')
html_parts.append("</table>")
in_table = False
html_parts.append('<br>')
html_parts.append("<br>")
else:
if in_table:
html_parts.append('</table>')
html_parts.append("</table>")
in_table = False
if line.strip():
html_parts.append(f'<p>{line}</p>')
html_parts.append(f"<p>{line}</p>")
if in_table:
html_parts.append('</table>')
html_parts.append("</table>")
html_parts.append('</body></html>')
return '\n'.join(html_parts)
html_parts.append("</body></html>")
return "\n".join(html_parts)
def _markdown_to_text(self, markdown_content: str) -> str:
"""
@@ -404,34 +404,34 @@ class DeleteProgressWindow:
Returns:
格式化后的文本
"""
lines = markdown_content.split('\n')
lines = markdown_content.split("\n")
result = []
for line in lines:
# 标题
if line.startswith('# '):
result.append('=' * 60)
if line.startswith("# "):
result.append("=" * 60)
result.append(line[2:])
result.append('=' * 60)
elif line.startswith('## '):
result.append('')
result.append("=" * 60)
elif line.startswith("## "):
result.append("")
result.append(line[3:])
result.append('-' * 40)
elif line.startswith('| '):
result.append("-" * 40)
elif line.startswith("| "):
# 表格行 - 保持原样
result.append(line)
elif line.startswith('|--') or line.startswith('|-'):
elif line.startswith("|--") or line.startswith("|-"):
# 表格分隔线 - 跳过
continue
elif line.startswith('- '):
elif line.startswith("- "):
# 列表项
result.append(' ' + line)
elif line.strip() == '':
result.append('')
result.append(" " + line)
elif line.strip() == "":
result.append("")
else:
result.append(line)
return '\n'.join(result)
return "\n".join(result)
def close(self):
"""关闭窗口"""
@@ -444,4 +444,4 @@ class DeleteProgressWindow:
def set_completed(self):
"""设置为完成状态"""
self.cancel_button.pack_forget()
self.close_button.pack(side=tk.RIGHT)
self.close_button.pack(side=tk.RIGHT)

View File

@@ -21,7 +21,7 @@ class FileSelector(ttk.Frame):
file_type: str = "file",
file_types: list = None,
initial_dir: str = "",
on_change: Optional[Callable] = None
on_change: Optional[Callable] = None,
):
"""
初始化文件选择器
@@ -64,15 +64,10 @@ class FileSelector(ttk.Frame):
if self.file_type == "file":
path = filedialog.askopenfilename(
title="选择文件",
initialdir=current_path,
filetypes=self.file_types
title="选择文件", initialdir=current_path, filetypes=self.file_types
)
else: # directory
path = filedialog.askdirectory(
title="选择目录",
initialdir=current_path
)
path = filedialog.askdirectory(title="选择目录", initialdir=current_path)
if path:
self.entry_var.set(path)

View File

@@ -3,6 +3,7 @@
"""
自定义 logging Handler将日志输出到 LogText 组件
"""
import logging
import re
from typing import Optional
@@ -29,11 +30,11 @@ class GuiTextHandler(logging.Handler):
# 映射 logging 级别到 LogText 级别
self.level_map = {
logging.INFO: 'INFO',
logging.WARNING: 'WARNING',
logging.ERROR: 'ERROR',
logging.DEBUG: 'DEBUG',
logging.CRITICAL: 'ERROR'
logging.INFO: "INFO",
logging.WARNING: "WARNING",
logging.ERROR: "ERROR",
logging.DEBUG: "DEBUG",
logging.CRITICAL: "ERROR",
}
def set_log_text(self, log_text: LogText):
@@ -57,7 +58,7 @@ class GuiTextHandler(logging.Handler):
try:
# 获取日志级别
level = self.level_map.get(record.levelno, 'INFO')
level = self.level_map.get(record.levelno, "INFO")
# 只获取消息内容不包含时间戳和级别LogText.log() 会添加)
message = record.getMessage()
@@ -78,6 +79,7 @@ class GuiTextHandler(logging.Handler):
# 尝试使用 after 确保在主线程更新
import tkinter as tk
try:
# 尝试获取主窗口
widget = self.log_text
@@ -112,9 +114,9 @@ class GuiTextHandler(logging.Handler):
清理后的消息
"""
# 常见的日志级别标记模式
level_pattern = r'^\[(?:INFO|WARNING|ERROR|DEBUG|CRITICAL|WARN|SUCCESS)\]\s*'
level_pattern = r"^\[(?:INFO|WARNING|ERROR|DEBUG|CRITICAL|WARN|SUCCESS)\]\s*"
match = re.match(level_pattern, message)
if match:
# 移除匹配到的级别前缀
return message[match.end():]
return message[match.end() :]
return message

View File

@@ -15,11 +15,11 @@ class LogText(tk.Frame):
# 日志级别颜色配置
LOG_COLORS = {
'INFO': '#000000', # 黑色
'SUCCESS': '#008000', # 绿色
'WARNING': '#FF8C00', # 深橙色
'ERROR': '#FF0000', # 红色
'DEBUG': '#808080', # 灰色
"INFO": "#000000", # 黑色
"SUCCESS": "#008000", # 绿色
"WARNING": "#FF8C00", # 深橙色
"ERROR": "#FF0000", # 红色
"DEBUG": "#808080", # 灰色
}
def __init__(self, parent, readonly=True, **kwargs):
@@ -67,19 +67,19 @@ class LogText(tk.Frame):
def _make_readonly(self):
"""通过绑定事件使文本框只读"""
# 允许复制、全选等常用操作,阻止其他编辑操作
self.text.bind('<Key>', self._handle_key)
self.text.bind('<Button-1>', self._allow_click) # 允许左键点击选择
self.text.bind("<Key>", self._handle_key)
self.text.bind("<Button-1>", self._allow_click) # 允许左键点击选择
def _handle_key(self, event):
"""处理按键事件,允许复制操作,阻止编辑"""
# 允许的快捷键
allowed_keys = [
'Control-c', # 复制
'Control-C', # 复制(大写)
'Control-a', # 全选
'Control-A', # 全选(大写)
'Control-x', # 剪切(虽然剪不了,但不报错)
'Control-X',
"Control-c", # 复制
"Control-C", # 复制(大写)
"Control-a", # 全选
"Control-A", # 全选(大写)
"Control-x", # 剪切(虽然剪不了,但不报错)
"Control-X",
]
# 检查是否是允许的快捷键
@@ -93,14 +93,14 @@ class LogText(tk.Frame):
return # 允许执行
# 其他所有按键都阻止
return 'break'
return "break"
def _allow_click(self, event):
"""允许点击和选择文本"""
# 不打断事件,允许正常的选择操作
return
def log(self, message: str, level: str = 'INFO') -> None:
def log(self, message: str, level: str = "INFO") -> None:
"""
添加日志消息
@@ -111,46 +111,46 @@ class LogText(tk.Frame):
# 确保 tags 已配置
self._ensure_tags_configured()
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"[{timestamp}] [{level}] {message}\n"
# 插入文本
tag = level.lower() if self._tags_configured else None
if tag:
try:
self.text.insert('end', log_message, (tag,))
self.text.insert("end", log_message, (tag,))
except Exception:
# 如果带标签插入失败,尝试不带标签
self.text.insert('end', log_message)
self.text.insert("end", log_message)
else:
self.text.insert('end', log_message)
self.text.insert("end", log_message)
# 自动滚动到底部
self.text.see('end')
self.text.see("end")
def info(self, message: str) -> None:
"""添加 INFO 级别日志"""
self.log(message, 'INFO')
self.log(message, "INFO")
def success(self, message: str) -> None:
"""添加 SUCCESS 级别日志"""
self.log(message, 'SUCCESS')
self.log(message, "SUCCESS")
def warning(self, message: str) -> None:
"""添加 WARNING 级别日志"""
self.log(message, 'WARNING')
self.log(message, "WARNING")
def error(self, message: str) -> None:
"""添加 ERROR 级别日志"""
self.log(message, 'ERROR')
self.log(message, "ERROR")
def debug(self, message: str) -> None:
"""添加 DEBUG 级别日志"""
self.log(message, 'DEBUG')
self.log(message, "DEBUG")
def clear(self) -> None:
"""清空日志"""
self.text.delete('1.0', 'end')
self.text.delete("1.0", "end")
def save_to_file(self, file_path: str) -> bool:
"""
@@ -163,8 +163,8 @@ class LogText(tk.Frame):
是否成功
"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.text.get('1.0', 'end-1c'))
with open(file_path, "w", encoding="utf-8") as f:
f.write(self.text.get("1.0", "end-1c"))
return True
except Exception as e:
self.error(f"保存日志失败: {e}")
@@ -182,5 +182,6 @@ class LogText(tk.Frame):
def apply_font(self, font_family: str, font_size: int):
"""应用字体设置"""
from tkinter import font as tk_font
font_spec = tk_font.Font(family=font_family, size=font_size)
self.text.configure(font=font_spec)

View File

@@ -35,12 +35,14 @@ class ProductionIdInput(ttk.Frame):
justify="center",
colors=("black", "#f0f0f0"),
bg="#f0f0f0",
width=3
width=3,
)
self.line_numbers.pack(fill="both", expand=True)
# 创建滚动条
self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text_widget.yview)
self.scrollbar = ttk.Scrollbar(
self, orient=tk.VERTICAL, command=self.text_widget.yview
)
self.text_widget.configure(yscrollcommand=self._on_scroll)
# 布局:行号 | 文本框 | 滚动条
@@ -74,7 +76,10 @@ class ProductionIdInput(ttk.Frame):
def _on_focus_in(self, event):
"""获得焦点时隐藏占位符"""
if not self._updating_placeholder and self.text_widget.get("1.0", "end-1c") == self.placeholder:
if (
not self._updating_placeholder
and self.text_widget.get("1.0", "end-1c") == self.placeholder
):
self.text_widget.delete("1.0", tk.END)
# 确保文字颜色为黑色
self.text_widget.configure(foreground="black")

View File

@@ -20,7 +20,7 @@ class ProgressDialog:
title: str = "处理中...",
message: str = "请稍候",
can_cancel: bool = True,
on_cancel: Optional[Callable] = None
on_cancel: Optional[Callable] = None,
):
"""
初始化进度对话框
@@ -66,11 +66,7 @@ class ProgressDialog:
self.message_label.pack(pady=(20, 10), padx=20)
# 进度条
self.progress = ttk.Progressbar(
self.dialog,
mode='indeterminate',
length=360
)
self.progress = ttk.Progressbar(self.dialog, mode="indeterminate", length=360)
self.progress.pack(pady=10, padx=20)
self.progress.start(10)
@@ -80,9 +76,7 @@ class ProgressDialog:
button_frame.pack(pady=10)
self.cancel_button = ttk.Button(
button_frame,
text="取消",
command=self._on_cancel
button_frame, text="取消", command=self._on_cancel
)
self.cancel_button.pack()
@@ -106,8 +100,8 @@ class ProgressDialog:
value: 当前进度值
maximum: 最大值
"""
self.progress.config(mode='determinate', maximum=maximum)
self.progress['value'] = value
self.progress.config(mode="determinate", maximum=maximum)
self.progress["value"] = value
self.dialog.update_idletasks()
def close(self):