feat: add delete execution feature with progress tracking and dryrun mode
- Add ExecutionConfig for dryrun settings in config schema - Create DeleteProgressWindow widget for real-time progress display - Integrate delete execution flow in MaterialValidationTab with threading - Add dryrun checkbox for admin users in settings - Add progress callback support to DiscreteMaterialPlanCleaner - Add markdown report generation with statistics - Include tkinterweb and markdown2 dependencies for report rendering Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
@@ -18,12 +18,13 @@ from pathlib import Path
|
||||
from io import StringIO
|
||||
from contextlib import redirect_stdout
|
||||
from typing import List, Dict
|
||||
from gui.widgets import FileSelector, LogText, GuiTextHandler
|
||||
from gui.widgets import FileSelector, LogText, GuiTextHandler, DeleteProgressWindow
|
||||
from gui.config_manager import ConfigManager
|
||||
from gui.log_config import setup_gui_logging, get_logger
|
||||
from gui.material_type_management_dialog import MaterialTypeManagementDialog
|
||||
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
|
||||
import pandas as pd
|
||||
import tempfile
|
||||
|
||||
|
||||
class CheckboxTreeview(ttk.Treeview):
|
||||
@@ -437,6 +438,25 @@ class MaterialValidationTab(ttk.Frame):
|
||||
)
|
||||
self.confirm_delete_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# 执行删除按钮
|
||||
self.execute_delete_button = ttk.Button(
|
||||
button_frame,
|
||||
text="执行删除",
|
||||
command=self.start_delete_execution,
|
||||
state=tk.DISABLED
|
||||
)
|
||||
self.execute_delete_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Admin 用户:显示 dryrun 复选框
|
||||
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
|
||||
)
|
||||
self.dryrun_checkbox.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.export_button = ttk.Button(
|
||||
button_frame,
|
||||
text="导出结果",
|
||||
@@ -556,6 +576,9 @@ class MaterialValidationTab(ttk.Frame):
|
||||
|
||||
# User 模式:静默更新,不显示任何提示
|
||||
if is_user_only:
|
||||
# 启用执行删除按钮(如果有 Production ID)
|
||||
if production_ids and hasattr(self, 'execute_delete_button'):
|
||||
self.execute_delete_button.config(state=tk.NORMAL)
|
||||
return
|
||||
|
||||
# Admin 模式:显示提示信息
|
||||
@@ -568,12 +591,18 @@ class MaterialValidationTab(ttk.Frame):
|
||||
# 显示提示标签
|
||||
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'):
|
||||
self.execute_delete_button.config(state=tk.NORMAL)
|
||||
else:
|
||||
self.shared_ids_info_label.config(text="")
|
||||
# 隐藏提示标签
|
||||
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'):
|
||||
self.execute_delete_button.config(state=tk.DISABLED)
|
||||
|
||||
def reload_config(self):
|
||||
"""配置更新后重新加载"""
|
||||
@@ -1062,6 +1091,14 @@ class MaterialValidationTab(ttk.Frame):
|
||||
self.deselect_all_button.config(state=tk.NORMAL)
|
||||
self.confirm_delete_button.config(state=tk.NORMAL)
|
||||
|
||||
# 启用执行删除按钮(需要 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.execute_delete_button.config(state=tk.NORMAL)
|
||||
|
||||
# 恢复筛选状态
|
||||
if hasattr(self, 'previously_selected_managers') and self.previously_selected_managers:
|
||||
self._restore_manager_filter_state(self.previously_selected_managers)
|
||||
@@ -1380,3 +1417,175 @@ class MaterialValidationTab(ttk.Frame):
|
||||
def open_type_management(self):
|
||||
"""打开类型管理窗口"""
|
||||
dialog = MaterialTypeManagementDialog(self, self.session_manager, title="物料类型管理")
|
||||
|
||||
def start_delete_execution(self):
|
||||
"""开始执行删除"""
|
||||
# 1. 获取 Production ID
|
||||
production_ids = None
|
||||
is_admin = self.session_manager.is_admin()
|
||||
|
||||
if is_admin:
|
||||
# Admin 用户:检查数据源选择
|
||||
if hasattr(self, 'production_id_source_var'):
|
||||
source = self.production_id_source_var.get()
|
||||
if source == "shared":
|
||||
production_ids = self.shared_production_ids
|
||||
else:
|
||||
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
|
||||
production_ids = self.shared_production_ids
|
||||
|
||||
# 验证 Production ID
|
||||
if not production_ids:
|
||||
messagebox.showerror("错误", "没有可用的 Production ID\n请先在校验页面获取数据")
|
||||
return
|
||||
|
||||
# 2. 获取负责人
|
||||
if is_admin:
|
||||
manager_names = self._get_selected_managers()
|
||||
if not manager_names:
|
||||
messagebox.showwarning("警告", "请至少选择一个负责人")
|
||||
return
|
||||
else:
|
||||
manager_names = [self.session_manager.get_username()]
|
||||
|
||||
# 3. 获取 dryrun 设置
|
||||
dryrun = False
|
||||
if is_admin and hasattr(self, 'dryrun_var'):
|
||||
# Admin 用户:使用界面上的 dryrun 复选框
|
||||
dryrun = self.dryrun_var.get()
|
||||
else:
|
||||
# User 用户:从配置读取 dryrun 设置
|
||||
dryrun = self.config.get("execution.dryrun", False)
|
||||
|
||||
# 4. 确认执行
|
||||
mode_text = "预览模式(不会保存更改)" if dryrun else "正式执行(将保存更改)"
|
||||
manager_text = "、".join(manager_names)
|
||||
confirm_msg = (
|
||||
f"确认执行删除操作?\n\n"
|
||||
f"模式: {mode_text}\n"
|
||||
f"负责人: {manager_text}\n"
|
||||
f"Production ID 数量: {len(production_ids)}\n\n"
|
||||
f"此操作可能需要较长时间,是否继续?"
|
||||
)
|
||||
if not messagebox.askyesno("确认执行", confirm_msg):
|
||||
return
|
||||
|
||||
# 5. 创建进度窗口
|
||||
self.progress_window = DeleteProgressWindow(
|
||||
self,
|
||||
title="执行删除",
|
||||
managers=manager_text,
|
||||
dryrun=dryrun,
|
||||
on_cancel=self._cancel_delete_execution
|
||||
)
|
||||
|
||||
self.log_text.info(f"开始执行删除(模式: {'预览' if dryrun else '正式'})...")
|
||||
|
||||
# 6. 启动后台线程执行
|
||||
delete_thread = threading.Thread(
|
||||
target=self._delete_worker,
|
||||
args=(production_ids, manager_names, dryrun),
|
||||
daemon=True
|
||||
)
|
||||
delete_thread.start()
|
||||
|
||||
def _cancel_delete_execution(self):
|
||||
"""取消删除执行"""
|
||||
self.log_text.info("用户取消了执行操作")
|
||||
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):
|
||||
"""
|
||||
后台线程执行删除
|
||||
|
||||
Args:
|
||||
production_ids: Production ID 列表
|
||||
manager_names: 负责人列表
|
||||
dryrun: 是否为预览模式
|
||||
"""
|
||||
progress_window = self.progress_window
|
||||
cancelled = False
|
||||
|
||||
try:
|
||||
from utils.discrete_material_plan_cleaner import DiscreteMaterialPlanCleaner
|
||||
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:
|
||||
temp_file = f.name
|
||||
f.write('\n'.join(production_ids))
|
||||
|
||||
# 进度回调函数
|
||||
def progress_callback(current: int, total: int, message: str):
|
||||
if progress_window and progress_window.is_cancelled():
|
||||
return
|
||||
progress_window.update_progress(current, total, message)
|
||||
progress_window.append_log(message, "info")
|
||||
|
||||
# 创建 Cleaner 实例
|
||||
cleaner = DiscreteMaterialPlanCleaner(
|
||||
username=self.config.get("erp.username"),
|
||||
password=self.config.get("erp.password"),
|
||||
manager_names=manager_names,
|
||||
headless=self.config.get("erp.headless", True),
|
||||
verbose=True,
|
||||
dryrun=dryrun,
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
|
||||
# 执行清理
|
||||
progress_window.append_log("正在连接 ERP 系统...", "info")
|
||||
cleaner.clean(temp_file)
|
||||
|
||||
# 检查是否被取消
|
||||
if progress_window.is_cancelled():
|
||||
cancelled = True
|
||||
self.after(0, lambda: self._update_log("执行已取消", "WARNING"))
|
||||
else:
|
||||
# 生成报告
|
||||
report = cleaner.generate_report()
|
||||
|
||||
# 在主线程中更新 UI
|
||||
self.after(0, lambda: self._delete_complete(report, cleaner.stats))
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"执行过程中发生错误: {str(e)}"
|
||||
self.after(0, lambda: self._update_log(error_msg, "ERROR"))
|
||||
if progress_window:
|
||||
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):
|
||||
os.unlink(temp_file)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _delete_complete(self, report: str, stats: dict):
|
||||
"""
|
||||
删除完成回调
|
||||
|
||||
Args:
|
||||
report: Markdown 格式的报告
|
||||
stats: 统计信息字典
|
||||
"""
|
||||
# 更新日志
|
||||
self.log_text.info("执行完成!")
|
||||
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:
|
||||
self.progress_window.show_report(report)
|
||||
|
||||
Reference in New Issue
Block a user