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:
@@ -297,6 +297,26 @@ class UIConfig:
|
||||
return errors
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExecutionConfig:
|
||||
"""执行配置(用于删除操作等)"""
|
||||
|
||||
dryrun: bool = False # 预览模式,不保存更改
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "ExecutionConfig":
|
||||
"""从环境变量创建配置"""
|
||||
from config.env_loader import get_env_bool
|
||||
|
||||
return cls(
|
||||
dryrun=get_env_bool("EXECUTION_DRYRUN", False),
|
||||
)
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
"""验证配置,返回错误列表"""
|
||||
return [] # dryrun 是布尔值,无需验证
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""应用总配置"""
|
||||
@@ -307,6 +327,7 @@ class AppConfig:
|
||||
extraction: ExtractionConfig
|
||||
validation: ValidationConfig
|
||||
ui: UIConfig
|
||||
execution: ExecutionConfig
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "AppConfig":
|
||||
@@ -318,6 +339,7 @@ class AppConfig:
|
||||
extraction=ExtractionConfig.from_env(),
|
||||
validation=ValidationConfig.from_env(),
|
||||
ui=UIConfig.from_env(),
|
||||
execution=ExecutionConfig.from_env(),
|
||||
)
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
@@ -329,6 +351,7 @@ class AppConfig:
|
||||
errors.extend(self.extraction.validate())
|
||||
errors.extend(self.validation.validate())
|
||||
errors.extend(self.ui.validate())
|
||||
errors.extend(self.execution.validate())
|
||||
return errors
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -384,4 +407,7 @@ class AppConfig:
|
||||
"font_size": self.ui.font_size,
|
||||
"production_id_input_width": self.ui.production_id_input_width,
|
||||
},
|
||||
"execution": {
|
||||
"dryrun": self.execution.dryrun,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -134,6 +134,11 @@ class ConfigManager:
|
||||
"""获取提取配置"""
|
||||
return self.config.extraction
|
||||
|
||||
@property
|
||||
def execution(self):
|
||||
"""获取执行配置"""
|
||||
return self.config.execution
|
||||
|
||||
|
||||
# 为了向后兼容,保留旧版本的导入
|
||||
DEFAULT_SETTINGS = DEFAULT_SETTINGS_DICT
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -63,12 +63,18 @@ class SettingsTab(ttk.Frame):
|
||||
self._create_validation_group(scrollable_frame)
|
||||
self._create_ui_group(scrollable_frame)
|
||||
else:
|
||||
# User 用户:只显示路径配置
|
||||
# User 用户:显示路径配置和执行设置
|
||||
self._create_paths_group(scrollable_frame)
|
||||
self._create_user_execution_group(scrollable_frame)
|
||||
|
||||
# 按钮区域 - 根据用户类型显示不同按钮
|
||||
button_frame = ttk.Frame(scrollable_frame)
|
||||
button_frame.grid(row=7, column=0, columnspan=2, pady=20, sticky="ew")
|
||||
if is_user_only:
|
||||
# User 用户:显示测试按钮和保存设置按钮(row=4 因为有执行设置组)
|
||||
button_frame.grid(row=4, column=0, columnspan=2, pady=20, sticky="ew")
|
||||
else:
|
||||
# 管理员显示所有按钮
|
||||
button_frame.grid(row=7, column=0, columnspan=2, pady=20, sticky="ew")
|
||||
|
||||
if is_user_only:
|
||||
# User 用户:显示测试按钮和保存设置按钮
|
||||
@@ -225,8 +231,14 @@ 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'
|
||||
|
||||
group = ttk.LabelFrame(parent, text="路径设置", padding=10)
|
||||
group.grid(row=2, column=1, pady=10, padx=10, sticky="nsew")
|
||||
if is_user_only:
|
||||
group.grid(row=2, column=1, pady=10, padx=10, sticky="nsew")
|
||||
else:
|
||||
group.grid(row=2, column=1, pady=10, padx=10, sticky="nsew")
|
||||
|
||||
from gui.widgets import FileSelector
|
||||
|
||||
@@ -255,6 +267,27 @@ class SettingsTab(ttk.Frame):
|
||||
|
||||
group.columnconfigure(0, weight=1)
|
||||
|
||||
def _create_user_execution_group(self, parent):
|
||||
"""创建 User 用户的执行设置组"""
|
||||
group = ttk.LabelFrame(parent, text="执行设置", padding=10)
|
||||
group.grid(row=3, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
|
||||
|
||||
# dryrun 模式设置
|
||||
self.user_dryrun_var = tk.BooleanVar(value=False)
|
||||
ttk.Checkbutton(
|
||||
group,
|
||||
text="预览模式 (执行删除时不保存更改)",
|
||||
variable=self.user_dryrun_var
|
||||
).grid(row=0, column=0, sticky="w", pady=5)
|
||||
|
||||
# 说明文字
|
||||
hint_label = ttk.Label(
|
||||
group,
|
||||
text="提示:勾选后,执行删除操作时将只预览不实际保存,用于测试流程。",
|
||||
foreground="gray"
|
||||
)
|
||||
hint_label.grid(row=1, column=0, sticky="w", pady=(0, 5))
|
||||
|
||||
def _create_extraction_group(self, parent):
|
||||
"""创建处理配置组"""
|
||||
group = ttk.LabelFrame(parent, text="数据提取设置", padding=10)
|
||||
@@ -398,6 +431,8 @@ class SettingsTab(ttk.Frame):
|
||||
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.user_dryrun_var.set(self.config.get("execution.dryrun", False))
|
||||
return
|
||||
|
||||
# 管理员模式 - 加载所有配置
|
||||
@@ -462,10 +497,12 @@ class SettingsTab(ttk.Frame):
|
||||
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
|
||||
|
||||
if is_user_only:
|
||||
# User 用户模式 - 只保存路径设置
|
||||
# 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("execution.dryrun", self.user_dryrun_var.get())
|
||||
|
||||
# 保存到文件
|
||||
if self.config.save():
|
||||
|
||||
@@ -8,5 +8,6 @@ from .file_selector import FileSelector
|
||||
from .log_text import LogText
|
||||
from .production_id_input import ProductionIdInput
|
||||
from .log_handler import GuiTextHandler
|
||||
from .delete_progress_window import DeleteProgressWindow
|
||||
|
||||
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput', 'GuiTextHandler']
|
||||
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput', 'GuiTextHandler', 'DeleteProgressWindow']
|
||||
|
||||
447
gui/widgets/delete_progress_window.py
Normal file
447
gui/widgets/delete_progress_window.py
Normal file
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
删除进度窗口组件
|
||||
|
||||
显示删除操作的进度和日志,完成后显示 Markdown 格式的报告。
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext
|
||||
from typing import Optional, Callable
|
||||
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
|
||||
|
||||
|
||||
class DeleteProgressWindow:
|
||||
"""删除进度窗口"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
title: str = "执行删除",
|
||||
managers: str = "",
|
||||
dryrun: bool = False,
|
||||
on_cancel: Optional[Callable] = None
|
||||
):
|
||||
"""
|
||||
初始化删除进度窗口
|
||||
|
||||
Args:
|
||||
parent: 父窗口
|
||||
title: 窗口标题
|
||||
managers: 负责人列表字符串
|
||||
dryrun: 是否为预览模式
|
||||
on_cancel: 取消回调函数
|
||||
"""
|
||||
self.parent = parent
|
||||
self.on_cancel = on_cancel
|
||||
self.cancelled = False
|
||||
self.managers = managers
|
||||
self.dryrun = dryrun
|
||||
|
||||
# 创建窗口
|
||||
self.window = tk.Toplevel(parent)
|
||||
self.window.title(title)
|
||||
self.window.resizable(True, True)
|
||||
self.window.transient(parent)
|
||||
|
||||
# 设置窗口大小
|
||||
self.window.geometry("700x600")
|
||||
|
||||
# 创建内容
|
||||
self._create_widgets()
|
||||
|
||||
# 居中显示
|
||||
self._center()
|
||||
|
||||
def _center(self):
|
||||
"""将窗口居中显示"""
|
||||
self.window.update_idletasks()
|
||||
width = 700
|
||||
height = 600
|
||||
x = (self.window.winfo_screenwidth() // 2) - (width // 2)
|
||||
y = (self.window.winfo_screenheight() // 2) - (height // 2)
|
||||
self.window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
def _create_widgets(self):
|
||||
"""创建窗口组件"""
|
||||
# 主容器
|
||||
self.main_frame = ttk.Frame(self.window, padding=10)
|
||||
self.main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 信息区域
|
||||
info_frame = ttk.Frame(self.main_frame)
|
||||
info_frame.pack(fill=tk.X, pady=(0, 10))
|
||||
|
||||
# 负责人信息
|
||||
if self.managers:
|
||||
ttk.Label(info_frame, text=f"负责人: {self.managers}").pack(anchor="w")
|
||||
|
||||
# 模式信息
|
||||
mode_text = "预览模式 (不保存)" if self.dryrun else "正常执行"
|
||||
mode_label = ttk.Label(info_frame, text=f"模式: {mode_text}")
|
||||
mode_label.pack(anchor="w")
|
||||
|
||||
# 进度区域
|
||||
self.progress_frame = ttk.LabelFrame(self.main_frame, text="进度", padding=5)
|
||||
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.pack(anchor="w")
|
||||
|
||||
self.progress_bar = ttk.Progressbar(
|
||||
self.progress_frame,
|
||||
mode='determinate',
|
||||
length=660,
|
||||
maximum=100
|
||||
)
|
||||
self.progress_bar.pack(fill=tk.X, pady=5)
|
||||
|
||||
# 日志区域(执行过程中显示)
|
||||
self.log_frame = ttk.LabelFrame(self.main_frame, text="日志", padding=5)
|
||||
self.log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(
|
||||
self.log_frame,
|
||||
height=10,
|
||||
wrap=tk.WORD,
|
||||
state=tk.DISABLED,
|
||||
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.report_frame = ttk.LabelFrame(self.main_frame, text="执行报告", padding=5)
|
||||
|
||||
# 根据 tkinterweb 可用性选择渲染方式
|
||||
if HAS_TKINTERWEB:
|
||||
# 使用 HtmlFrame 渲染 HTML
|
||||
self.report_html = HtmlFrame(self.report_frame)
|
||||
self.report_html.pack(fill=tk.BOTH, expand=True)
|
||||
else:
|
||||
# 降级为文本显示
|
||||
self.report_text = scrolledtext.ScrolledText(
|
||||
self.report_frame,
|
||||
height=20,
|
||||
wrap=tk.WORD,
|
||||
state=tk.DISABLED,
|
||||
font=('Consolas', 9)
|
||||
)
|
||||
self.report_text.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# 按钮区域
|
||||
button_frame = ttk.Frame(self.main_frame)
|
||||
button_frame.pack(fill=tk.X)
|
||||
|
||||
self.cancel_button = ttk.Button(
|
||||
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
|
||||
)
|
||||
|
||||
def _on_cancel(self):
|
||||
"""处理取消操作"""
|
||||
self.cancelled = True
|
||||
self.cancel_button.config(state=tk.DISABLED, text="正在取消...")
|
||||
if self.on_cancel:
|
||||
self.on_cancel()
|
||||
else:
|
||||
self.append_log("用户取消了操作", "warning")
|
||||
|
||||
def update_progress(self, current: int, total: int, message: str):
|
||||
"""
|
||||
更新进度
|
||||
|
||||
Args:
|
||||
current: 当前进度值
|
||||
total: 总数
|
||||
message: 进度消息
|
||||
"""
|
||||
if total > 0:
|
||||
percentage = int((current / total) * 100)
|
||||
self.progress_bar['value'] = percentage
|
||||
self.progress_var.set(message)
|
||||
else:
|
||||
self.progress_var.set(message)
|
||||
self.window.update_idletasks()
|
||||
|
||||
def append_log(self, message: str, level: str = "info"):
|
||||
"""
|
||||
追加日志
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别 (info, success, warning, error)
|
||||
"""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
log_entry = f"[{timestamp}] {message}\n"
|
||||
|
||||
self.log_text.config(state=tk.NORMAL)
|
||||
self.log_text.insert(tk.END, log_entry, level)
|
||||
self.log_text.see(tk.END)
|
||||
self.log_text.config(state=tk.DISABLED)
|
||||
self.window.update_idletasks()
|
||||
|
||||
def show_report(self, markdown_content: str):
|
||||
"""
|
||||
显示报告
|
||||
|
||||
Args:
|
||||
markdown_content: Markdown 格式的报告内容
|
||||
"""
|
||||
# 隐藏进度区域和日志区域
|
||||
self.progress_frame.pack_forget()
|
||||
self.log_frame.pack_forget()
|
||||
|
||||
# 显示报告区域
|
||||
self.report_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||||
|
||||
# 根据可用库选择渲染方式
|
||||
if HAS_TKINTERWEB and HAS_MARKDOWN2:
|
||||
# 使用 tkinterweb 渲染 HTML
|
||||
html_content = self._markdown_to_html(markdown_content)
|
||||
self.report_html.load_html(html_content)
|
||||
elif HAS_TKINTERWEB:
|
||||
# 只有 tkinterweb,使用简单 HTML
|
||||
html_content = self._markdown_to_simple_html(markdown_content)
|
||||
self.report_html.load_html(html_content)
|
||||
else:
|
||||
# 降级为文本显示
|
||||
text_content = self._markdown_to_text(markdown_content)
|
||||
self.report_text.config(state=tk.NORMAL)
|
||||
self.report_text.delete(1.0, tk.END)
|
||||
self.report_text.insert(tk.END, text_content)
|
||||
self.report_text.config(state=tk.DISABLED)
|
||||
|
||||
# 更新标题
|
||||
self.window.title("执行报告")
|
||||
|
||||
# 隐藏取消按钮,显示关闭按钮
|
||||
self.cancel_button.pack_forget()
|
||||
self.close_button.pack(side=tk.RIGHT)
|
||||
|
||||
# 更新进度标签
|
||||
self.progress_var.set("执行完成")
|
||||
|
||||
def _markdown_to_html(self, markdown_content: str) -> str:
|
||||
"""
|
||||
将 Markdown 转换为 HTML
|
||||
|
||||
Args:
|
||||
markdown_content: Markdown 内容
|
||||
|
||||
Returns:
|
||||
HTML 内容
|
||||
"""
|
||||
# 使用 markdown2 转换
|
||||
html_body = markdown2.markdown(
|
||||
markdown_content,
|
||||
extras=['tables', 'fenced-code-blocks']
|
||||
)
|
||||
|
||||
# 添加样式
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
line-height: 1.6;
|
||||
background-color: #ffffff;
|
||||
}}
|
||||
h1 {{
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 10px;
|
||||
font-size: 18px;
|
||||
}}
|
||||
h2 {{
|
||||
color: #34495e;
|
||||
border-bottom: 1px solid #bdc3c7;
|
||||
padding-bottom: 5px;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}}
|
||||
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;
|
||||
}}
|
||||
tr:nth-child(even) {{
|
||||
background-color: #f2f2f2;
|
||||
}}
|
||||
ul {{
|
||||
list-style-type: disc;
|
||||
padding-left: 20px;
|
||||
}}
|
||||
li {{
|
||||
margin: 5px 0;
|
||||
}}
|
||||
.success {{ color: #27ae60; }}
|
||||
.warning {{ color: #f39c12; }}
|
||||
.error {{ color: #e74c3c; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html_body}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return html_content
|
||||
|
||||
def _markdown_to_simple_html(self, markdown_content: str) -> str:
|
||||
"""
|
||||
将 Markdown 转换为简单 HTML(不依赖 markdown2)
|
||||
|
||||
Args:
|
||||
markdown_content: Markdown 内容
|
||||
|
||||
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>']
|
||||
|
||||
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 not in_table:
|
||||
html_parts.append('<table>')
|
||||
in_table = True
|
||||
# 检查是否是表头分隔行
|
||||
if '|--' in line or '|-' in line:
|
||||
continue
|
||||
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>')
|
||||
else:
|
||||
html_parts.append('<tr>' + ''.join(f'<td>{c}</td>' for c in cells) + '</tr>')
|
||||
elif line.startswith('- '):
|
||||
if in_table:
|
||||
html_parts.append('</table>')
|
||||
in_table = False
|
||||
html_parts.append(f'<li>{line[2:]}</li>')
|
||||
elif line.strip() == '':
|
||||
if in_table:
|
||||
html_parts.append('</table>')
|
||||
in_table = False
|
||||
html_parts.append('<br>')
|
||||
else:
|
||||
if in_table:
|
||||
html_parts.append('</table>')
|
||||
in_table = False
|
||||
if line.strip():
|
||||
html_parts.append(f'<p>{line}</p>')
|
||||
|
||||
if in_table:
|
||||
html_parts.append('</table>')
|
||||
|
||||
html_parts.append('</body></html>')
|
||||
return '\n'.join(html_parts)
|
||||
|
||||
def _markdown_to_text(self, markdown_content: str) -> str:
|
||||
"""
|
||||
将 Markdown 转换为简单的文本格式
|
||||
|
||||
Args:
|
||||
markdown_content: Markdown 内容
|
||||
|
||||
Returns:
|
||||
格式化后的文本
|
||||
"""
|
||||
lines = markdown_content.split('\n')
|
||||
result = []
|
||||
|
||||
for line in lines:
|
||||
# 标题
|
||||
if line.startswith('# '):
|
||||
result.append('=' * 60)
|
||||
result.append(line[2:])
|
||||
result.append('=' * 60)
|
||||
elif line.startswith('## '):
|
||||
result.append('')
|
||||
result.append(line[3:])
|
||||
result.append('-' * 40)
|
||||
elif line.startswith('| '):
|
||||
# 表格行 - 保持原样
|
||||
result.append(line)
|
||||
elif line.startswith('|--') or line.startswith('|-'):
|
||||
# 表格分隔线 - 跳过
|
||||
continue
|
||||
elif line.startswith('- '):
|
||||
# 列表项
|
||||
result.append(' ' + line)
|
||||
elif line.strip() == '':
|
||||
result.append('')
|
||||
else:
|
||||
result.append(line)
|
||||
|
||||
return '\n'.join(result)
|
||||
|
||||
def close(self):
|
||||
"""关闭窗口"""
|
||||
self.window.destroy()
|
||||
|
||||
def is_cancelled(self) -> bool:
|
||||
"""检查是否已取消"""
|
||||
return self.cancelled
|
||||
|
||||
def set_completed(self):
|
||||
"""设置为完成状态"""
|
||||
self.cancel_button.pack_forget()
|
||||
self.close_button.pack(side=tk.RIGHT)
|
||||
@@ -20,3 +20,7 @@ python-dateutil>=2.8.0
|
||||
pytz>=2023.0
|
||||
python-dotenv>=1.0.0
|
||||
tklinenums>=1.7.0
|
||||
|
||||
# --- Markdown Rendering ---
|
||||
markdown2>=2.4.0
|
||||
tkinterweb>=3.23
|
||||
|
||||
@@ -11,7 +11,8 @@ import os
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from typing import Union, List, Optional
|
||||
from typing import Union, List, Optional, Callable, Dict, Any
|
||||
from datetime import datetime
|
||||
from playwright.sync_api import sync_playwright, TimeoutError
|
||||
|
||||
# 统一顶部导入
|
||||
@@ -42,12 +43,14 @@ class DiscreteMaterialPlanCleaner:
|
||||
headless=False,
|
||||
verbose=True,
|
||||
dryrun=False,
|
||||
progress_callback: Optional[Callable[[int, int, str], None]] = None,
|
||||
):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.headless = headless
|
||||
self.verbose = verbose
|
||||
self.dryrun = dryrun
|
||||
self.progress_callback = progress_callback
|
||||
|
||||
# 参数规范化:支持 str、List[str]、None
|
||||
if manager_names is None:
|
||||
@@ -60,6 +63,19 @@ class DiscreteMaterialPlanCleaner:
|
||||
# 核心优化:使用 set 存储待删除编码,查询复杂度为 $O(1)$
|
||||
self.to_delete_set = set()
|
||||
|
||||
# 统计信息
|
||||
self.stats = {
|
||||
'total_orders': 0,
|
||||
'processed_orders': 0,
|
||||
'total_materials': 0, # 总物料数
|
||||
'processed_materials': 0, # 已处理物料数
|
||||
'deleted_materials': [], # [{order_id, material_code, material_name}]
|
||||
'skipped_materials': [], # [{order_id, material_code, material_name, reason}]
|
||||
'errors': [], # [{order_id, error_message}]
|
||||
'start_time': None,
|
||||
'end_time': None,
|
||||
}
|
||||
|
||||
def _log(self, message, level="info"):
|
||||
"""统一日志输出控制"""
|
||||
if self.verbose:
|
||||
@@ -70,6 +86,47 @@ class DiscreteMaterialPlanCleaner:
|
||||
elif level == "error":
|
||||
logger.error(message)
|
||||
|
||||
def _report_progress(self, current: int, total: int, message: str):
|
||||
"""报告进度"""
|
||||
if self.progress_callback:
|
||||
self.progress_callback(current, total, message)
|
||||
|
||||
def _report_material_progress(
|
||||
self,
|
||||
order_idx: int,
|
||||
total_orders: int,
|
||||
material_idx: int,
|
||||
total_materials: int,
|
||||
order_id: str,
|
||||
material_name: str,
|
||||
action: str
|
||||
):
|
||||
"""报告物料处理进度
|
||||
|
||||
进度计算逻辑:
|
||||
- 每个订单占 1/total_orders 的固定进度配额
|
||||
- 订单内物料进度按比例分配(material_idx / total_materials)
|
||||
- 总体进度 = (order_idx + material_idx / total_materials) / total_orders
|
||||
"""
|
||||
if self.progress_callback:
|
||||
order_progress = f"订单 [{order_idx + 1}/{total_orders}]"
|
||||
if total_materials > 0:
|
||||
material_progress = f"物料 [{material_idx}/{total_materials}]"
|
||||
message = f"{order_progress} {material_progress} - {order_id} - {action}: {material_name}"
|
||||
|
||||
# 计算总体进度比例(0.0 到 1.0)
|
||||
order_internal_ratio = material_idx / total_materials
|
||||
overall_ratio = (order_idx + order_internal_ratio) / total_orders
|
||||
|
||||
# 使用固定精度整数表示进度(范围 0-10000,显示时除以 100 即为百分比)
|
||||
PROGRESS_SCALE = 10000
|
||||
overall_current = int(overall_ratio * PROGRESS_SCALE)
|
||||
overall_total = PROGRESS_SCALE
|
||||
self.progress_callback(overall_current, overall_total, message)
|
||||
else:
|
||||
message = f"{order_progress} - {order_id} - {action}: {material_name}"
|
||||
self.progress_callback(order_idx + 1, total_orders, message)
|
||||
|
||||
def _is_button_enabled(self, button_locator):
|
||||
"""判定按钮是否可用"""
|
||||
try:
|
||||
@@ -108,8 +165,19 @@ class DiscreteMaterialPlanCleaner:
|
||||
)
|
||||
return order_ids
|
||||
|
||||
def process_order(self, inner_frame, order_id, order_index, page1):
|
||||
"""清理单个订单的数据"""
|
||||
def process_order(self, inner_frame, order_id, order_index, page1, total_orders: int = 1):
|
||||
"""清理单个订单的数据
|
||||
|
||||
Args:
|
||||
inner_frame: 内层 iframe
|
||||
order_id: 订单 ID
|
||||
order_index: 订单索引(从 0 开始)
|
||||
page1: 页面对象
|
||||
total_orders: 总订单数(用于进度报告)
|
||||
"""
|
||||
# 报告订单进度
|
||||
# self._report_progress(order_index + 1, total_orders, f"正在打开订单: {order_id}")
|
||||
|
||||
# 1. 查询订单
|
||||
textbox = inner_frame.get_by_role("textbox", name="生产订单号")
|
||||
textbox.fill(order_id)
|
||||
@@ -137,7 +205,7 @@ class DiscreteMaterialPlanCleaner:
|
||||
plan_code_locator = detail_inner_frame.get_by_text(
|
||||
re.compile(r"^离散备料计划维护:")
|
||||
)
|
||||
plan_code_locator.wait_for(state="visible", timeout=15000)
|
||||
plan_code_locator.wait_for(state="visible", timeout=30000)
|
||||
|
||||
detail_count_text = detail_inner_frame.get_by_text(
|
||||
re.compile(r"^详细信息 \(\d+\)$")
|
||||
@@ -154,6 +222,9 @@ class DiscreteMaterialPlanCleaner:
|
||||
# 6. 执行清理逻辑
|
||||
if detail_status == "审批通过":
|
||||
if detail_count > 0:
|
||||
# 更新总物料数统计
|
||||
self.stats['total_materials'] += detail_count
|
||||
|
||||
# --- 点击修改并等待状态切换 (保留原逻辑) ---
|
||||
detail_inner_frame.get_by_role("button", name="修改").click()
|
||||
|
||||
@@ -161,7 +232,7 @@ class DiscreteMaterialPlanCleaner:
|
||||
save_button_locator = detail_inner_frame.get_by_role(
|
||||
"button", name="保存"
|
||||
)
|
||||
save_button_locator.wait_for(state="visible", timeout=10000)
|
||||
save_button_locator.wait_for(state="visible", timeout=30000)
|
||||
self._log("已进入编辑模式(保存按钮已就绪)")
|
||||
# ---------------------------------------
|
||||
|
||||
@@ -175,8 +246,12 @@ class DiscreteMaterialPlanCleaner:
|
||||
collapse_btn = button_wrapper.locator(".icon-celashouqi")
|
||||
|
||||
last_row_number = None
|
||||
material_idx = 0 # 物料计数器
|
||||
# page2.pause() # 调试用,正式运行时可删除
|
||||
while True:
|
||||
material_idx += 1
|
||||
self.stats['processed_materials'] += 1
|
||||
|
||||
# 稳定性检查:等待行号更新
|
||||
current_row = self._get_input_value(child_form, r"^行号$")
|
||||
row_num_int = int(current_row)
|
||||
@@ -187,11 +262,24 @@ class DiscreteMaterialPlanCleaner:
|
||||
material_name = self._get_input_value(child_form, r"^材料名称")
|
||||
pending_qty = self._get_input_value(child_form, r"^累计待发数量$")
|
||||
|
||||
# 报告物料进度
|
||||
self._report_material_progress(
|
||||
order_index, total_orders,
|
||||
material_idx, detail_count,
|
||||
order_id, material_name, "检查"
|
||||
)
|
||||
|
||||
if material_code in self.to_delete_set:
|
||||
self._log(f"发现匹配物料: {material_name} ({material_code})")
|
||||
if (not pending_qty) and not (
|
||||
row_num_int >= 7000 and row_num_int < 8000
|
||||
):
|
||||
# 报告删除进度
|
||||
self._report_material_progress(
|
||||
order_index, total_orders,
|
||||
material_idx, detail_count,
|
||||
order_id, material_name, "删除"
|
||||
)
|
||||
# 记录删除前的行号
|
||||
old_row_number = current_row
|
||||
delete_row_btn.click()
|
||||
@@ -200,6 +288,7 @@ class DiscreteMaterialPlanCleaner:
|
||||
# 等待行号变化(表示删除完成且新数据已加载)
|
||||
max_wait_time = 10 # 最大等待10秒
|
||||
start_time = time.time()
|
||||
delete_success = False
|
||||
while time.time() - start_time < max_wait_time:
|
||||
try:
|
||||
new_row_number = self._get_input_value(
|
||||
@@ -209,6 +298,7 @@ class DiscreteMaterialPlanCleaner:
|
||||
self._log(
|
||||
f"✓ 删除完成,行号已从 {old_row_number} 变更为 {new_row_number}"
|
||||
)
|
||||
delete_success = True
|
||||
break
|
||||
time.sleep(0.2) # 每200ms检查一次
|
||||
except Exception as e:
|
||||
@@ -219,20 +309,61 @@ class DiscreteMaterialPlanCleaner:
|
||||
f"⚠️ 等待删除完成超时({max_wait_time}秒)", "warn"
|
||||
)
|
||||
|
||||
# 记录删除统计
|
||||
if delete_success:
|
||||
self.stats['deleted_materials'].append({
|
||||
'order_id': order_id,
|
||||
'material_code': material_code,
|
||||
'material_name': material_name
|
||||
})
|
||||
|
||||
continue
|
||||
elif row_num_int >= 7000 and row_num_int < 8000:
|
||||
self._log(
|
||||
f"⚠️ 行号 {row_num_int} 在 7000-8000 范围内,跳过删除",
|
||||
"warn",
|
||||
reason = f"行号 {row_num_int} 在 7000-8000 范围内"
|
||||
self._report_material_progress(
|
||||
order_index, total_orders,
|
||||
material_idx, detail_count,
|
||||
order_id, material_name, "跳过"
|
||||
)
|
||||
self._log(f"⚠️ {reason},跳过删除", "warn")
|
||||
self.stats['skipped_materials'].append({
|
||||
'order_id': order_id,
|
||||
'material_code': material_code,
|
||||
'material_name': material_name,
|
||||
'reason': reason
|
||||
})
|
||||
elif pending_qty:
|
||||
self._log(f"⚠️ 待发数量为 {pending_qty},跳过删除", "warn")
|
||||
reason = f"待发数量为 {pending_qty}"
|
||||
self._report_material_progress(
|
||||
order_index, total_orders,
|
||||
material_idx, detail_count,
|
||||
order_id, material_name, "跳过"
|
||||
)
|
||||
self._log(f"⚠️ {reason},跳过删除", "warn")
|
||||
self.stats['skipped_materials'].append({
|
||||
'order_id': order_id,
|
||||
'material_code': material_code,
|
||||
'material_name': material_name,
|
||||
'reason': reason
|
||||
})
|
||||
|
||||
else:
|
||||
reason = "不满足删除条件"
|
||||
self._report_material_progress(
|
||||
order_index, total_orders,
|
||||
material_idx, detail_count,
|
||||
order_id, material_name, "跳过"
|
||||
)
|
||||
self._log(
|
||||
f"⚠️ 不满足删除条件,跳过物料 {material_name} ({material_code})",
|
||||
f"⚠️ {reason},跳过物料 {material_name} ({material_code})",
|
||||
"warn",
|
||||
)
|
||||
self.stats['skipped_materials'].append({
|
||||
'order_id': order_id,
|
||||
'material_code': material_code,
|
||||
'material_name': material_name,
|
||||
'reason': reason
|
||||
})
|
||||
|
||||
else:
|
||||
self._log(
|
||||
@@ -276,6 +407,9 @@ class DiscreteMaterialPlanCleaner:
|
||||
|
||||
def clean(self, production_id_file):
|
||||
"""执行完整清理流程"""
|
||||
# 初始化统计
|
||||
self.stats['start_time'] = datetime.now()
|
||||
|
||||
# 0. 预加载数据库数据
|
||||
self.preload_data()
|
||||
|
||||
@@ -300,19 +434,27 @@ class DiscreteMaterialPlanCleaner:
|
||||
work_main_frame = page1.locator("#forwardFrame").content_frame
|
||||
inner_frame = work_main_frame.locator("#mainiframe").content_frame
|
||||
inner_frame.locator("#hot-key-head_list").wait_for(
|
||||
state="visible", timeout=15000
|
||||
state="visible", timeout=30000
|
||||
)
|
||||
|
||||
self.setup_query_interface(inner_frame)
|
||||
order_ids = self.get_production_order_numbers(production_id_file)
|
||||
|
||||
# 设置总订单数
|
||||
self.stats['total_orders'] = len(order_ids)
|
||||
|
||||
# 遍历处理
|
||||
for index, order_id in enumerate(order_ids):
|
||||
self._log(f"进度: [{index+1}/{len(order_ids)}] 处理单号: {order_id}")
|
||||
try:
|
||||
self.process_order(inner_frame, order_id, index, page1)
|
||||
self.process_order(inner_frame, order_id, index, page1, len(order_ids))
|
||||
self.stats['processed_orders'] += 1
|
||||
except Exception as e:
|
||||
self._log(f"处理单号 {order_id} 时发生异常: {e}", "error")
|
||||
self.stats['errors'].append({
|
||||
'order_id': order_id,
|
||||
'error_message': str(e)
|
||||
})
|
||||
continue # 单个失败不影响整体执行
|
||||
|
||||
# 登出清理
|
||||
@@ -321,6 +463,74 @@ class DiscreteMaterialPlanCleaner:
|
||||
browser.close()
|
||||
self._log("=" * 30 + " 任务全部完成 " + "=" * 30)
|
||||
|
||||
# 记录结束时间
|
||||
self.stats['end_time'] = datetime.now()
|
||||
|
||||
def generate_report(self) -> str:
|
||||
"""生成 Markdown 格式的执行报告
|
||||
|
||||
Returns:
|
||||
Markdown 格式的报告字符串
|
||||
"""
|
||||
report_lines = []
|
||||
|
||||
# 标题
|
||||
report_lines.append("# 执行报告")
|
||||
report_lines.append("")
|
||||
|
||||
# 概述
|
||||
report_lines.append("## 概述")
|
||||
report_lines.append("")
|
||||
start_time = self.stats.get('start_time')
|
||||
end_time = self.stats.get('end_time')
|
||||
duration = None
|
||||
if start_time and end_time:
|
||||
duration = end_time - start_time
|
||||
report_lines.append(f"- 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
report_lines.append(f"- 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
report_lines.append(f"- 执行时长: {duration}")
|
||||
report_lines.append(f"- 处理订单: {self.stats['processed_orders']}/{self.stats['total_orders']} 个")
|
||||
report_lines.append(f"- 处理物料: {self.stats['processed_materials']}/{self.stats['total_materials']} 条")
|
||||
report_lines.append(f"- 删除物料: {len(self.stats['deleted_materials'])} 条")
|
||||
report_lines.append(f"- 跳过物料: {len(self.stats['skipped_materials'])} 条")
|
||||
report_lines.append(f"- 错误数量: {len(self.stats['errors'])} 个")
|
||||
report_lines.append(f"- 执行模式: {'预览模式 (dryrun)' if self.dryrun else '正常执行'}")
|
||||
if self.manager_names:
|
||||
report_lines.append(f"- 负责人: {', '.join(self.manager_names)}")
|
||||
report_lines.append("")
|
||||
|
||||
# 删除明细
|
||||
if self.stats['deleted_materials']:
|
||||
report_lines.append("## 删除明细")
|
||||
report_lines.append("")
|
||||
report_lines.append("| 订单号 | 物料编码 | 物料名称 |")
|
||||
report_lines.append("|--------|----------|----------|")
|
||||
for item in self.stats['deleted_materials']:
|
||||
report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |")
|
||||
report_lines.append("")
|
||||
|
||||
# 跳过明细
|
||||
if self.stats['skipped_materials']:
|
||||
report_lines.append("## 跳过明细")
|
||||
report_lines.append("")
|
||||
report_lines.append("| 订单号 | 物料编码 | 物料名称 | 跳过原因 |")
|
||||
report_lines.append("|--------|----------|----------|----------|")
|
||||
for item in self.stats['skipped_materials']:
|
||||
report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} | {item['reason']} |")
|
||||
report_lines.append("")
|
||||
|
||||
# 错误明细
|
||||
if self.stats['errors']:
|
||||
report_lines.append("## 错误明细")
|
||||
report_lines.append("")
|
||||
report_lines.append("| 订单号 | 错误信息 |")
|
||||
report_lines.append("|--------|----------|")
|
||||
for item in self.stats['errors']:
|
||||
report_lines.append(f"| {item['order_id']} | {item['error_message']} |")
|
||||
report_lines.append("")
|
||||
|
||||
return "\n".join(report_lines)
|
||||
|
||||
|
||||
def main():
|
||||
# 路径配置
|
||||
|
||||
Reference in New Issue
Block a user