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:
@@ -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