Compare commits
10 Commits
b9184bbe3e
...
3c45ef58d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c45ef58d1 | ||
|
|
85be5e166b | ||
|
|
ac94b1cb82 | ||
|
|
ed7eaf038b | ||
|
|
82819ae9d1 | ||
|
|
3b491d7b36 | ||
|
|
28d7db26db | ||
|
|
ee0b4187d7 | ||
|
|
6f8df2f2e6 | ||
|
|
62f323d420 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,4 +31,5 @@ record.py
|
|||||||
|
|
||||||
# 环境变量
|
# 环境变量
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
.secrets.toml
|
||||||
@@ -269,6 +269,34 @@ class ValidationConfig:
|
|||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UIConfig:
|
||||||
|
"""用户界面配置"""
|
||||||
|
|
||||||
|
font_family: str = "Microsoft YaHei UI"
|
||||||
|
font_size: int = 10
|
||||||
|
production_id_input_width: int = 20
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_env(cls) -> "UIConfig":
|
||||||
|
"""从环境变量创建配置"""
|
||||||
|
from config.env_loader import get_env, get_env_int
|
||||||
|
return cls(
|
||||||
|
font_family=get_env("UI_FONT_FAMILY", "Microsoft YaHei UI"),
|
||||||
|
font_size=get_env_int("UI_FONT_SIZE", 10),
|
||||||
|
production_id_input_width=get_env_int("UI_PRODUCTION_ID_INPUT_WIDTH", 20),
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self) -> list[str]:
|
||||||
|
"""验证配置,返回错误列表"""
|
||||||
|
errors = []
|
||||||
|
if self.font_size < 8 or self.font_size > 24:
|
||||||
|
errors.append("字号必须在 8-24 之间")
|
||||||
|
if self.production_id_input_width < 10 or self.production_id_input_width > 100:
|
||||||
|
errors.append("输入框宽度必须在 10-100 之间")
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
"""应用总配置"""
|
"""应用总配置"""
|
||||||
@@ -278,6 +306,7 @@ class AppConfig:
|
|||||||
paths: PathConfig
|
paths: PathConfig
|
||||||
extraction: ExtractionConfig
|
extraction: ExtractionConfig
|
||||||
validation: ValidationConfig
|
validation: ValidationConfig
|
||||||
|
ui: UIConfig
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_env(cls) -> "AppConfig":
|
def from_env(cls) -> "AppConfig":
|
||||||
@@ -288,6 +317,7 @@ class AppConfig:
|
|||||||
paths=PathConfig.from_env(),
|
paths=PathConfig.from_env(),
|
||||||
extraction=ExtractionConfig.from_env(),
|
extraction=ExtractionConfig.from_env(),
|
||||||
validation=ValidationConfig.from_env(),
|
validation=ValidationConfig.from_env(),
|
||||||
|
ui=UIConfig.from_env(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self) -> list[str]:
|
def validate(self) -> list[str]:
|
||||||
@@ -298,6 +328,7 @@ class AppConfig:
|
|||||||
errors.extend(self.paths.validate())
|
errors.extend(self.paths.validate())
|
||||||
errors.extend(self.extraction.validate())
|
errors.extend(self.extraction.validate())
|
||||||
errors.extend(self.validation.validate())
|
errors.extend(self.validation.validate())
|
||||||
|
errors.extend(self.ui.validate())
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
@@ -348,4 +379,9 @@ class AppConfig:
|
|||||||
"default_manager": self.validation.default_manager,
|
"default_manager": self.validation.default_manager,
|
||||||
"match_mode": self.validation.match_mode,
|
"match_mode": self.validation.match_mode,
|
||||||
},
|
},
|
||||||
|
"ui": {
|
||||||
|
"font_family": self.ui.font_family,
|
||||||
|
"font_size": self.ui.font_size,
|
||||||
|
"production_id_input_width": self.ui.production_id_input_width,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from db.connection import get_connection
|
from db.connection import get_connection
|
||||||
|
from config.schema import DatabaseType
|
||||||
|
from config.loader import ConfigLoader
|
||||||
|
|
||||||
|
|
||||||
def read_production_ids(file_path):
|
def read_production_ids(file_path):
|
||||||
@@ -35,6 +37,10 @@ def query_production_order_numbers(production_ids):
|
|||||||
if not production_ids:
|
if not production_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# 获取当前数据库类型
|
||||||
|
app_config = ConfigLoader.load()
|
||||||
|
db_type = app_config.database.db_type
|
||||||
|
|
||||||
# SQL Server 限制每个查询最多 2100 个参数
|
# SQL Server 限制每个查询最多 2100 个参数
|
||||||
BATCH_SIZE = 2000
|
BATCH_SIZE = 2000
|
||||||
all_results = []
|
all_results = []
|
||||||
@@ -42,18 +48,31 @@ def query_production_order_numbers(production_ids):
|
|||||||
# 分批查询
|
# 分批查询
|
||||||
for i in range(0, len(production_ids), BATCH_SIZE):
|
for i in range(0, len(production_ids), BATCH_SIZE):
|
||||||
batch = production_ids[i : i + BATCH_SIZE]
|
batch = production_ids[i : i + BATCH_SIZE]
|
||||||
placeholders = ",".join(["?" for _ in batch])
|
|
||||||
|
|
||||||
query = f"""
|
|
||||||
SELECT [生产订单号]
|
|
||||||
FROM [productionContractData].[26年压力表合同数据]
|
|
||||||
WHERE [总排号] IN ({placeholders})
|
|
||||||
"""
|
|
||||||
|
|
||||||
with get_connection() as conn:
|
with get_connection() as conn:
|
||||||
|
# 获取正确的占位符
|
||||||
|
placeholder = conn.get_placeholder()
|
||||||
|
placeholders = ",".join([placeholder for _ in batch])
|
||||||
|
|
||||||
|
# 根据数据库类型选择表名和列名格式
|
||||||
|
if db_type == DatabaseType.MYSQL:
|
||||||
|
table_name = "productionContractData_26年压力表合同数据"
|
||||||
|
query = f"""
|
||||||
|
SELECT 生产订单号
|
||||||
|
FROM {table_name}
|
||||||
|
WHERE 总排号 IN ({placeholders})
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
table_name = "[productionContractData].[26年压力表合同数据]"
|
||||||
|
query = f"""
|
||||||
|
SELECT [生产订单号]
|
||||||
|
FROM {table_name}
|
||||||
|
WHERE [总排号] IN ({placeholders})
|
||||||
|
"""
|
||||||
|
|
||||||
results = conn.execute_query(query, tuple(batch))
|
results = conn.execute_query(query, tuple(batch))
|
||||||
# 提取生产订单号并去除空值
|
# 提取生产订单号并去除空值
|
||||||
batch_numbers = [row["生产订单号"] for row in results if row["生产订单号"]]
|
batch_numbers = [row["生产订单号"] for row in results if row.get("生产订单号")]
|
||||||
all_results.extend(batch_numbers)
|
all_results.extend(batch_numbers)
|
||||||
|
|
||||||
return all_results
|
return all_results
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
数据提取标签页
|
数据提取标签页 - 稳定性修复版
|
||||||
|
|
||||||
从 ERP 系统提取备料计划数据。
|
修复了 LogText.info 不支持 add_timestamp 参数导致的 TypeError。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -13,8 +13,7 @@ import queue
|
|||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from contextlib import redirect_stdout
|
from gui.widgets import FileSelector, LogText, ProductionIdInput
|
||||||
from gui.widgets import FileSelector, LogText
|
|
||||||
from gui.config_manager import ConfigManager
|
from gui.config_manager import ConfigManager
|
||||||
from gui.progress import ProgressInfo, ProgressCalculator
|
from gui.progress import ProgressInfo, ProgressCalculator
|
||||||
from gui.utils import RealtimeOutput
|
from gui.utils import RealtimeOutput
|
||||||
@@ -23,182 +22,147 @@ from gui.utils import RealtimeOutput
|
|||||||
class DataExtractionTab(ttk.Frame):
|
class DataExtractionTab(ttk.Frame):
|
||||||
"""数据提取标签页"""
|
"""数据提取标签页"""
|
||||||
|
|
||||||
def __init__(self, parent, config: ConfigManager):
|
def __init__(self, parent, config: ConfigManager, main_window=None):
|
||||||
"""
|
|
||||||
初始化数据提取标签页
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: 父容器
|
|
||||||
config: 配置管理器
|
|
||||||
"""
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self.main_window = main_window
|
||||||
self.extracting = False
|
self.extracting = False
|
||||||
self.extractor = None
|
self.extractor = None
|
||||||
self.extraction_thread = None
|
self.extraction_thread = None
|
||||||
self.progress_calculator = ProgressCalculator()
|
self.progress_calculator = ProgressCalculator()
|
||||||
self.progress_queue = queue.Queue() # 进度更新队列
|
self.progress_queue = queue.Queue()
|
||||||
|
|
||||||
# 启动进度更新轮询
|
|
||||||
self._poll_progress_queue()
|
self._poll_progress_queue()
|
||||||
|
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
self._apply_ui_config()
|
||||||
|
|
||||||
# 稍后显示就绪消息
|
|
||||||
try:
|
try:
|
||||||
self.log_text.info("数据提取标签页已就绪")
|
self.log_text.info("数据提取标签页已就绪")
|
||||||
except:
|
except:
|
||||||
pass # 如果窗口还未完全就绪,忽略错误
|
pass
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""创建界面组件"""
|
horizontal_paned = ttk.PanedWindow(self, orient=tk.HORIZONTAL)
|
||||||
# 主容器 - 使用 PanedWindow 分割上下部分
|
horizontal_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||||
main_paned = ttk.PanedWindow(self, orient=tk.VERTICAL)
|
|
||||||
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# 上部:控制面板
|
left_panel = ttk.Frame(horizontal_paned)
|
||||||
|
horizontal_paned.add(left_panel, weight=0)
|
||||||
|
|
||||||
|
right_panel = ttk.Frame(horizontal_paned)
|
||||||
|
horizontal_paned.add(right_panel, weight=1)
|
||||||
|
|
||||||
|
self._create_left_panel(left_panel)
|
||||||
|
self._create_right_panel(right_panel)
|
||||||
|
|
||||||
|
self.horizontal_paned = horizontal_paned
|
||||||
|
input_width = self.config.get("ui.production_id_input_width", 20)
|
||||||
|
self.after(100, lambda: self._set_pane_width(input_width * 8))
|
||||||
|
|
||||||
|
def _create_left_panel(self, parent):
|
||||||
|
input_group = ttk.LabelFrame(parent, text="Production ID", padding=10)
|
||||||
|
input_group.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.production_id_input = ProductionIdInput(
|
||||||
|
input_group,
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _create_right_panel(self, parent):
|
||||||
|
main_paned = ttk.PanedWindow(parent, orient=tk.VERTICAL)
|
||||||
|
main_paned.pack(fill=tk.BOTH, expand=True)
|
||||||
control_frame = ttk.Frame(main_paned)
|
control_frame = ttk.Frame(main_paned)
|
||||||
main_paned.add(control_frame, weight=0)
|
main_paned.add(control_frame, weight=0)
|
||||||
|
|
||||||
# 下部:日志输出
|
|
||||||
log_frame = ttk.LabelFrame(main_paned, text="日志输出", padding=5)
|
log_frame = ttk.LabelFrame(main_paned, text="日志输出", padding=5)
|
||||||
main_paned.add(log_frame, weight=1)
|
main_paned.add(log_frame, weight=1)
|
||||||
|
|
||||||
self._create_control_panel(control_frame)
|
self._create_control_panel(control_frame)
|
||||||
self._create_log_panel(log_frame)
|
self._create_log_panel(log_frame)
|
||||||
|
|
||||||
def _create_control_panel(self, parent):
|
def _create_control_panel(self, parent):
|
||||||
"""创建控制面板"""
|
|
||||||
# 输入文件选择
|
|
||||||
input_group = ttk.LabelFrame(parent, text="输入文件", padding=10)
|
|
||||||
input_group.pack(fill=tk.X, pady=5)
|
|
||||||
|
|
||||||
self.input_file_selector = FileSelector(
|
|
||||||
input_group,
|
|
||||||
label_text="ProductionID 文件:",
|
|
||||||
file_type="file",
|
|
||||||
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
|
||||||
initial_dir="D:/python/playwrite/",
|
|
||||||
)
|
|
||||||
self.input_file_selector.pack(fill=tk.X)
|
|
||||||
|
|
||||||
# 设置默认文件
|
|
||||||
default_input = self.config.get("paths.production_id_file", "ProductionID.txt")
|
|
||||||
if os.path.exists(default_input):
|
|
||||||
self.input_file_selector.set(default_input)
|
|
||||||
|
|
||||||
# 输出文件选择
|
|
||||||
output_group = ttk.LabelFrame(parent, text="输出文件", padding=10)
|
output_group = ttk.LabelFrame(parent, text="输出文件", padding=10)
|
||||||
output_group.pack(fill=tk.X, pady=5)
|
output_group.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
self.output_file_selector = FileSelector(
|
self.output_file_selector = FileSelector(
|
||||||
output_group,
|
output_group, label_text="保存为:", file_type="file",
|
||||||
label_text="保存为:",
|
|
||||||
file_type="file",
|
|
||||||
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
||||||
initial_dir=self.config.get("paths.data_dir", "data/"),
|
initial_dir=self.config.get("paths.data_dir", "data/"),
|
||||||
)
|
)
|
||||||
self.output_file_selector.pack(fill=tk.X)
|
self.output_file_selector.pack(fill=tk.X)
|
||||||
|
|
||||||
# 设置默认输出文件
|
|
||||||
default_output = os.path.join(
|
default_output = os.path.join(
|
||||||
self.config.get("paths.data_dir", "data/"),
|
self.config.get("paths.data_dir", "data/"),
|
||||||
self.config.get("paths.default_output", "离散备料计划维护_合并.xlsx"),
|
self.config.get("paths.default_output", "离散备料计划维护_合并.xlsx"),
|
||||||
)
|
)
|
||||||
self.output_file_selector.set(default_output)
|
self.output_file_selector.set(default_output)
|
||||||
|
|
||||||
# 选项
|
|
||||||
options_group = ttk.LabelFrame(parent, text="提取选项", padding=10)
|
options_group = ttk.LabelFrame(parent, text="提取选项", padding=10)
|
||||||
options_group.pack(fill=tk.X, pady=5)
|
options_group.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
self.headless_var = tk.BooleanVar(value=self.config.get("erp.headless", True))
|
self.headless_var = tk.BooleanVar(value=self.config.get("erp.headless", True))
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(options_group, text="无头模式", variable=self.headless_var).grid(row=0, column=0, sticky="w", padx=5)
|
||||||
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 = ttk.LabelFrame(parent, text="进度", padding=10)
|
||||||
progress_group.pack(fill=tk.X, pady=5)
|
progress_group.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
self.progress_bar = ttk.Progressbar(progress_group, mode="determinate")
|
self.progress_bar = ttk.Progressbar(progress_group, mode="determinate")
|
||||||
self.progress_bar.pack(fill=tk.X, pady=5)
|
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)
|
self.status_label.pack(fill=tk.X)
|
||||||
|
|
||||||
# 控制按钮
|
|
||||||
button_frame = ttk.Frame(parent)
|
button_frame = ttk.Frame(parent)
|
||||||
button_frame.pack(fill=tk.X, pady=10)
|
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.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)
|
self.stop_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
def _create_log_panel(self, parent):
|
def _create_log_panel(self, parent):
|
||||||
"""创建日志面板"""
|
|
||||||
self.log_text = LogText(parent, height=15, readonly=True)
|
self.log_text = LogText(parent, height=15, readonly=True)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
def _apply_ui_config(self):
|
||||||
|
try:
|
||||||
|
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'):
|
||||||
|
self.log_text.apply_font(font_family, font_size)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def _set_pane_width(self, width: int):
|
||||||
|
try: self.horizontal_paned.sashpos(0, width)
|
||||||
|
except: pass
|
||||||
|
|
||||||
def start_extraction(self):
|
def start_extraction(self):
|
||||||
"""开始数据提取"""
|
production_ids = self.production_id_input.get()
|
||||||
# 验证输入
|
if not production_ids:
|
||||||
input_file = self.input_file_selector.get()
|
messagebox.showerror("错误", "请输入至少一个 Production ID")
|
||||||
|
return
|
||||||
output_file = self.output_file_selector.get()
|
output_file = self.output_file_selector.get()
|
||||||
|
|
||||||
if not input_file:
|
|
||||||
messagebox.showerror("错误", "请选择 ProductionID 输入文件")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not os.path.exists(input_file):
|
|
||||||
messagebox.showerror("错误", f"输入文件不存在:{input_file}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not output_file:
|
if not output_file:
|
||||||
messagebox.showerror("错误", "请指定输出文件路径")
|
messagebox.showerror("错误", "请指定输出文件路径")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 确保输出目录存在
|
|
||||||
output_dir = os.path.dirname(output_file)
|
|
||||||
if output_dir and not os.path.exists(output_dir):
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 更新 UI 状态
|
|
||||||
self.extracting = True
|
self.extracting = True
|
||||||
self.start_button.config(state=tk.DISABLED)
|
self.start_button.config(state=tk.DISABLED)
|
||||||
self.stop_button.config(state=tk.NORMAL)
|
self.stop_button.config(state=tk.NORMAL)
|
||||||
self.progress_bar["value"] = 0
|
self.progress_bar["value"] = 0
|
||||||
self.status_label.config(text="正在登录...")
|
self.status_label.config(text="正在初始化...")
|
||||||
self.log_text.clear()
|
self.log_text.clear()
|
||||||
self.log_text.info("开始数据提取...")
|
|
||||||
|
|
||||||
# 在后台线程中执行提取
|
|
||||||
self.extraction_thread = threading.Thread(
|
self.extraction_thread = threading.Thread(
|
||||||
target=self._extraction_worker, args=(input_file, output_file), daemon=True
|
target=self._extraction_worker, args=(production_ids, output_file), daemon=True
|
||||||
)
|
)
|
||||||
self.extraction_thread.start()
|
self.extraction_thread.start()
|
||||||
|
|
||||||
def stop_extraction(self):
|
def stop_extraction(self):
|
||||||
"""停止数据提取"""
|
|
||||||
if self.extracting:
|
if self.extracting:
|
||||||
self.extracting = False
|
self.extracting = False
|
||||||
self.log_text.warning("正在停止提取...")
|
self.log_text.warning("正在停止提取...")
|
||||||
self.status_label.config(text="正在停止...")
|
|
||||||
|
|
||||||
def _extraction_worker(self, input_file: str, output_file: str):
|
def _extraction_worker(self, production_ids: list[str], output_file: str):
|
||||||
"""提取工作线程"""
|
import tempfile
|
||||||
|
temp_file = None
|
||||||
try:
|
try:
|
||||||
# 导入提取器(延迟导入以避免启动时加载 Playwright)
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
|
||||||
from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor
|
temp_file = f.name
|
||||||
|
f.write('\n'.join(production_ids))
|
||||||
|
|
||||||
# 创建提取器实例
|
from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor
|
||||||
self.extractor = DiscreteMaterialPlanExtractor(
|
self.extractor = DiscreteMaterialPlanExtractor(
|
||||||
username=self.config.get("erp.username"),
|
username=self.config.get("erp.username"),
|
||||||
password=self.config.get("erp.password"),
|
password=self.config.get("erp.password"),
|
||||||
@@ -208,82 +172,67 @@ class DataExtractionTab(ttk.Frame):
|
|||||||
enable_db_persistence=self.config.get("extraction.enable_db_persistence", False),
|
enable_db_persistence=self.config.get("extraction.enable_db_persistence", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建实时输出流,每次写入立即更新 GUI
|
# 修复:直接调用标准的 _update_log,不再传入 add_timestamp 参数
|
||||||
realtime_output = RealtimeOutput(
|
def progress_callback(progress_info: ProgressInfo):
|
||||||
lambda line: self._update_log(line, "INFO")
|
if progress_info.stage == "log":
|
||||||
|
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)
|
||||||
|
self._update_progress(percent, progress_info.message)
|
||||||
|
|
||||||
|
result = self.extractor.extract(
|
||||||
|
production_id_file=temp_file, output_file=output_file, progress_callback=progress_callback
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建进度回调函数
|
|
||||||
def progress_callback(progress_info: ProgressInfo):
|
|
||||||
# 计算总体进度百分比
|
|
||||||
overall_percent = self.progress_calculator.calculate_overall_percent(
|
|
||||||
progress_info
|
|
||||||
)
|
|
||||||
self._update_progress(overall_percent, progress_info.message)
|
|
||||||
|
|
||||||
# 重定向 stdout 并执行提取(带进度回调)
|
|
||||||
with redirect_stdout(realtime_output):
|
|
||||||
result = self.extractor.extract(
|
|
||||||
production_id_file=input_file,
|
|
||||||
output_file=output_file,
|
|
||||||
progress_callback=progress_callback,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result and self.extracting:
|
if result and self.extracting:
|
||||||
self._update_log(f"数据已保存到:{output_file}", "SUCCESS")
|
self._update_log("数据处理任务圆满结束", "SUCCESS")
|
||||||
elif not self.extracting:
|
elif not self.extracting:
|
||||||
self._update_log("提取已取消", "WARNING")
|
self._update_log("提取已取消", "WARNING")
|
||||||
else:
|
|
||||||
self._update_log("提取失败", "ERROR")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._update_log(f"提取过程中发生错误:{str(e)}", "ERROR")
|
self._update_log(f"运行时错误: {str(e)}", "ERROR")
|
||||||
finally:
|
finally:
|
||||||
# 更新 UI 状态
|
if temp_file and os.path.exists(temp_file):
|
||||||
|
try: os.unlink(temp_file)
|
||||||
|
except: pass
|
||||||
self.after(0, self._extraction_complete)
|
self.after(0, self._extraction_complete)
|
||||||
|
|
||||||
def _extraction_complete(self):
|
def _extraction_complete(self):
|
||||||
"""提取完成后的 UI 更新"""
|
|
||||||
self.extracting = False
|
self.extracting = False
|
||||||
self.start_button.config(state=tk.NORMAL)
|
self.start_button.config(state=tk.NORMAL)
|
||||||
self.stop_button.config(state=tk.DISABLED)
|
self.stop_button.config(state=tk.DISABLED)
|
||||||
self.extractor = None
|
self.extractor = None
|
||||||
|
|
||||||
def _poll_progress_queue(self):
|
def _poll_progress_queue(self):
|
||||||
"""轮询进度队列,处理进度更新"""
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
# 非阻塞地获取队列中的消息
|
|
||||||
try:
|
try:
|
||||||
progress_data = self.progress_queue.get_nowait()
|
value, message = self.progress_queue.get_nowait()
|
||||||
value, message = progress_data
|
|
||||||
self.progress_bar["value"] = value
|
self.progress_bar["value"] = value
|
||||||
self.status_label.config(text=message)
|
self.status_label.config(text=message)
|
||||||
except queue.Empty:
|
except queue.Empty: break
|
||||||
break
|
finally: self.after(50, self._poll_progress_queue)
|
||||||
finally:
|
|
||||||
# 继续轮询(每 50ms 检查一次)
|
|
||||||
self.after(50, self._poll_progress_queue)
|
|
||||||
|
|
||||||
def _update_progress(self, value: int, message: str):
|
def _update_progress(self, value: int, message: str):
|
||||||
"""线程安全的进度更新(通过队列)"""
|
try: self.progress_queue.put_nowait((value, message))
|
||||||
try:
|
except: pass
|
||||||
self.progress_queue.put_nowait((value, message))
|
|
||||||
except:
|
|
||||||
pass # 队列满时忽略
|
|
||||||
|
|
||||||
def _update_log(self, message: str, level: str = "INFO"):
|
def _update_log(self, message: str, level: str = "INFO"):
|
||||||
"""线程安全的日志更新"""
|
"""标准的日志更新方法"""
|
||||||
|
|
||||||
def update():
|
def update():
|
||||||
|
# 即使任务结束,只要是成功/错误消息也强制显示
|
||||||
if self.extracting or level in ["ERROR", "WARNING", "SUCCESS"]:
|
if self.extracting or level in ["ERROR", "WARNING", "SUCCESS"]:
|
||||||
if level == "INFO":
|
if level == "INFO": self.log_text.info(message)
|
||||||
self.log_text.info(message)
|
elif level == "SUCCESS": self.log_text.success(message)
|
||||||
elif level == "SUCCESS":
|
elif level == "WARNING": self.log_text.warning(message)
|
||||||
self.log_text.success(message)
|
elif level == "ERROR": self.log_text.error(message)
|
||||||
elif level == "WARNING":
|
|
||||||
self.log_text.warning(message)
|
|
||||||
elif level == "ERROR":
|
|
||||||
self.log_text.error(message)
|
|
||||||
|
|
||||||
self.after(0, update)
|
self.after(0, update)
|
||||||
|
|
||||||
|
def _on_production_ids_changed(self, event=None):
|
||||||
|
if self.main_window:
|
||||||
|
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()
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
数据查询标签页
|
|
||||||
|
|
||||||
查询生产订单号等信息。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import threading
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, messagebox, filedialog
|
|
||||||
from gui.widgets import LogText
|
|
||||||
from gui.config_manager import ConfigManager
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
class DataQueryTab(ttk.Frame):
|
|
||||||
"""数据查询标签页"""
|
|
||||||
|
|
||||||
def __init__(self, parent, config: ConfigManager):
|
|
||||||
"""
|
|
||||||
初始化数据查询标签页
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parent: 父容器
|
|
||||||
config: 配置管理器
|
|
||||||
"""
|
|
||||||
super().__init__(parent)
|
|
||||||
self.config = config
|
|
||||||
self.query_results = None
|
|
||||||
self.querying = False
|
|
||||||
|
|
||||||
self.create_widgets()
|
|
||||||
|
|
||||||
# 稍后显示就绪消息
|
|
||||||
try:
|
|
||||||
self.log_text.info("数据查询标签页已就绪")
|
|
||||||
except:
|
|
||||||
pass # 如果窗口还未完全就绪,忽略错误
|
|
||||||
|
|
||||||
def create_widgets(self):
|
|
||||||
"""创建界面组件"""
|
|
||||||
# 主容器
|
|
||||||
main_container = ttk.Frame(self)
|
|
||||||
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
||||||
|
|
||||||
# 上部:查询控制
|
|
||||||
control_frame = ttk.LabelFrame(main_container, text="查询条件", padding=10)
|
|
||||||
control_frame.pack(fill=tk.X, pady=(0, 10))
|
|
||||||
|
|
||||||
# 中部:结果表格
|
|
||||||
result_frame = ttk.LabelFrame(main_container, text="查询结果", padding=5)
|
|
||||||
result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
|
||||||
|
|
||||||
# 下部:日志
|
|
||||||
log_frame = ttk.LabelFrame(main_container, text="日志", padding=5)
|
|
||||||
log_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
self._create_query_control(control_frame)
|
|
||||||
self._create_result_table(result_frame)
|
|
||||||
self._create_log_panel(log_frame)
|
|
||||||
|
|
||||||
def _create_query_control(self, parent):
|
|
||||||
"""创建查询控制面板"""
|
|
||||||
# 查询说明
|
|
||||||
info_label = ttk.Label(
|
|
||||||
parent,
|
|
||||||
text="输入总排号列表(每行一个),查询对应的生产订单号信息",
|
|
||||||
foreground="#666666",
|
|
||||||
)
|
|
||||||
info_label.pack(anchor=tk.W, pady=(0, 5))
|
|
||||||
|
|
||||||
# 输入区域
|
|
||||||
input_frame = ttk.Frame(parent)
|
|
||||||
input_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# 左侧:文本输入框
|
|
||||||
text_frame = ttk.Frame(input_frame)
|
|
||||||
text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
|
|
||||||
|
|
||||||
self.input_text = tk.Text(text_frame, height=10, wrap=tk.WORD)
|
|
||||||
self.input_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
text_scrollbar = ttk.Scrollbar(
|
|
||||||
text_frame, orient=tk.VERTICAL, command=self.input_text.yview
|
|
||||||
)
|
|
||||||
text_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
self.input_text.configure(yscrollcommand=text_scrollbar.set)
|
|
||||||
|
|
||||||
# 右侧:示例和按钮
|
|
||||||
right_frame = ttk.Frame(input_frame, width=200)
|
|
||||||
right_frame.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
# 示例文本
|
|
||||||
example_label = ttk.Label(right_frame, text="示例格式:", foreground="#666666")
|
|
||||||
example_label.pack(anchor=tk.W, pady=(0, 5))
|
|
||||||
|
|
||||||
example_text = tk.Text(right_frame, height=8, width=25, wrap=tk.WORD)
|
|
||||||
example_text.pack(fill=tk.X)
|
|
||||||
example_text.insert("1.0", "24000001\n24000002\n24000003")
|
|
||||||
example_text.config(state=tk.DISABLED)
|
|
||||||
|
|
||||||
# 快捷按钮
|
|
||||||
ttk.Separator(right_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10)
|
|
||||||
|
|
||||||
ttk.Button(
|
|
||||||
right_frame, text="加载 ProductionID.txt", command=self._load_production_id
|
|
||||||
).pack(fill=tk.X, pady=2)
|
|
||||||
ttk.Button(right_frame, text="清空输入", command=self._clear_input).pack(
|
|
||||||
fill=tk.X, pady=2
|
|
||||||
)
|
|
||||||
|
|
||||||
# 查询按钮
|
|
||||||
button_frame = ttk.Frame(parent)
|
|
||||||
button_frame.pack(fill=tk.X, pady=(10, 0))
|
|
||||||
|
|
||||||
self.query_button = ttk.Button(
|
|
||||||
button_frame, text="执行查询", command=self.execute_query
|
|
||||||
)
|
|
||||||
self.query_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
self.export_button = ttk.Button(
|
|
||||||
button_frame,
|
|
||||||
text="导出结果",
|
|
||||||
command=self.export_results,
|
|
||||||
state=tk.DISABLED,
|
|
||||||
)
|
|
||||||
self.export_button.pack(side=tk.LEFT, padx=5)
|
|
||||||
|
|
||||||
self.progress = ttk.Progressbar(parent, mode="indeterminate")
|
|
||||||
self.progress.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
|
|
||||||
|
|
||||||
def _create_result_table(self, parent):
|
|
||||||
"""创建结果表格"""
|
|
||||||
# 创建 Treeview
|
|
||||||
columns = ("总排号", "生产订单号", "订单状态")
|
|
||||||
self.tree = ttk.Treeview(parent, columns=columns, show="headings", height=12)
|
|
||||||
|
|
||||||
# 设置列标题和宽度
|
|
||||||
self.tree.heading("总排号", text="总排号")
|
|
||||||
self.tree.heading("生产订单号", text="生产订单号")
|
|
||||||
self.tree.heading("订单状态", text="订单状态")
|
|
||||||
|
|
||||||
self.tree.column("总排号", width=150)
|
|
||||||
self.tree.column("生产订单号", width=300)
|
|
||||||
self.tree.column("订单状态", width=150)
|
|
||||||
|
|
||||||
# 添加滚动条
|
|
||||||
scrollbar_y = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.tree.yview)
|
|
||||||
scrollbar_x = ttk.Scrollbar(
|
|
||||||
parent, orient=tk.HORIZONTAL, command=self.tree.xview
|
|
||||||
)
|
|
||||||
|
|
||||||
self.tree.configure(
|
|
||||||
yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set
|
|
||||||
)
|
|
||||||
|
|
||||||
# 布局
|
|
||||||
self.tree.grid(row=0, column=0, sticky="nsew")
|
|
||||||
scrollbar_y.grid(row=0, column=1, sticky="ns")
|
|
||||||
scrollbar_x.grid(row=1, column=0, sticky="ew")
|
|
||||||
|
|
||||||
parent.rowconfigure(0, weight=1)
|
|
||||||
parent.columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
def _create_log_panel(self, parent):
|
|
||||||
"""创建日志面板"""
|
|
||||||
self.log_text = LogText(parent, height=6, readonly=True)
|
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
def _load_production_id(self):
|
|
||||||
"""加载 ProductionID.txt 文件"""
|
|
||||||
default_path = self.config.get("paths.production_id_file", "ProductionID.txt")
|
|
||||||
|
|
||||||
# 检查默认路径
|
|
||||||
if os.path.exists(default_path):
|
|
||||||
file_path = default_path
|
|
||||||
else:
|
|
||||||
# 打开文件选择对话框
|
|
||||||
from tkinter import filedialog
|
|
||||||
|
|
||||||
file_path = filedialog.askopenfilename(
|
|
||||||
title="选择 ProductionID 文件",
|
|
||||||
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not file_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
self.input_text.delete("1.0", tk.END)
|
|
||||||
self.input_text.insert("1.0", content)
|
|
||||||
self.log_text.info(f"已加载文件:{file_path}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("错误", f"加载文件失败:{str(e)}")
|
|
||||||
self.log_text.error(f"加载文件失败:{str(e)}")
|
|
||||||
|
|
||||||
def _clear_input(self):
|
|
||||||
"""清空输入"""
|
|
||||||
self.input_text.delete("1.0", tk.END)
|
|
||||||
|
|
||||||
def execute_query(self):
|
|
||||||
"""执行查询"""
|
|
||||||
# 获取输入的总排号列表
|
|
||||||
input_text = self.input_text.get("1.0", tk.END).strip()
|
|
||||||
|
|
||||||
if not input_text:
|
|
||||||
messagebox.showwarning("警告", "请输入总排号列表")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 解析总排号
|
|
||||||
production_ids = [
|
|
||||||
line.strip() for line in input_text.split("\n") if line.strip()
|
|
||||||
]
|
|
||||||
|
|
||||||
if not production_ids:
|
|
||||||
messagebox.showwarning("警告", "没有有效的总排号")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.log_text.info(f"准备查询 {len(production_ids)} 个总排号...")
|
|
||||||
|
|
||||||
# 更新 UI 状态
|
|
||||||
self.querying = True
|
|
||||||
self.query_button.config(state=tk.DISABLED)
|
|
||||||
self.progress.start(10)
|
|
||||||
|
|
||||||
# 清空结果表格
|
|
||||||
for item in self.tree.get_children():
|
|
||||||
self.tree.delete(item)
|
|
||||||
|
|
||||||
# 在后台线程中执行查询
|
|
||||||
query_thread = threading.Thread(
|
|
||||||
target=self._query_worker, args=(production_ids,), daemon=True
|
|
||||||
)
|
|
||||||
query_thread.start()
|
|
||||||
|
|
||||||
def _query_worker(self, production_ids: list):
|
|
||||||
"""查询工作线程"""
|
|
||||||
try:
|
|
||||||
# 导入查询函数
|
|
||||||
from db.production_order_query import query_production_order_numbers
|
|
||||||
|
|
||||||
self._update_log("正在连接数据库...", "INFO")
|
|
||||||
|
|
||||||
# 执行查询
|
|
||||||
results = query_production_order_numbers(production_ids)
|
|
||||||
|
|
||||||
if results:
|
|
||||||
self._update_log(f"查询完成,共 {len(results)} 条结果", "SUCCESS")
|
|
||||||
self._display_results(results)
|
|
||||||
else:
|
|
||||||
self._update_log("查询完成,但没有找到匹配的结果", "WARNING")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self._update_log(f"查询过程中发生错误:{str(e)}", "ERROR")
|
|
||||||
messagebox.showerror("错误", f"查询失败:{str(e)}")
|
|
||||||
finally:
|
|
||||||
# 更新 UI 状态
|
|
||||||
self.after(0, self._query_complete)
|
|
||||||
|
|
||||||
def _query_complete(self):
|
|
||||||
"""查询完成后的 UI 更新"""
|
|
||||||
self.querying = False
|
|
||||||
self.query_button.config(state=tk.NORMAL)
|
|
||||||
self.progress.stop()
|
|
||||||
|
|
||||||
def _display_results(self, results: list):
|
|
||||||
"""在主线程中显示结果"""
|
|
||||||
|
|
||||||
def update():
|
|
||||||
for 总排号, 生产订单号 in results:
|
|
||||||
self.tree.insert("", tk.END, values=(总排号, 生产订单号, ""))
|
|
||||||
|
|
||||||
if len(results) > 0:
|
|
||||||
self.export_button.config(state=tk.NORMAL)
|
|
||||||
self.query_results = results
|
|
||||||
|
|
||||||
self.after(0, update)
|
|
||||||
|
|
||||||
def _update_log(self, message: str, level: str = "INFO"):
|
|
||||||
"""线程安全的日志更新"""
|
|
||||||
|
|
||||||
def update():
|
|
||||||
if level == "INFO":
|
|
||||||
self.log_text.info(message)
|
|
||||||
elif level == "SUCCESS":
|
|
||||||
self.log_text.success(message)
|
|
||||||
elif level == "WARNING":
|
|
||||||
self.log_text.warning(message)
|
|
||||||
elif level == "ERROR":
|
|
||||||
self.log_text.error(message)
|
|
||||||
|
|
||||||
self.after(0, update)
|
|
||||||
|
|
||||||
def export_results(self):
|
|
||||||
"""导出结果到 Excel"""
|
|
||||||
if not self.query_results:
|
|
||||||
messagebox.showwarning("警告", "没有结果可导出")
|
|
||||||
return
|
|
||||||
|
|
||||||
output_file = filedialog.asksaveasfilename(
|
|
||||||
title="导出查询结果",
|
|
||||||
defaultextension=".xlsx",
|
|
||||||
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
|
||||||
initialfile="生产订单号查询结果.xlsx",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not output_file:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 创建 DataFrame 并保存
|
|
||||||
df = pd.DataFrame(self.query_results, columns=["总排号", "生产订单号"])
|
|
||||||
df.to_excel(output_file, index=False)
|
|
||||||
|
|
||||||
messagebox.showinfo("成功", f"结果已导出到:{output_file}")
|
|
||||||
self.log_text.success(f"结果已导出到:{output_file}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
messagebox.showerror("错误", f"导出失败:{str(e)}")
|
|
||||||
self.log_text.error(f"导出失败:{str(e)}")
|
|
||||||
@@ -11,7 +11,6 @@ from tkinter import ttk
|
|||||||
from gui.config_manager import ConfigManager
|
from gui.config_manager import ConfigManager
|
||||||
from gui.data_extraction_tab import DataExtractionTab
|
from gui.data_extraction_tab import DataExtractionTab
|
||||||
from gui.material_validation_tab import MaterialValidationTab
|
from gui.material_validation_tab import MaterialValidationTab
|
||||||
from gui.data_query_tab import DataQueryTab
|
|
||||||
from gui.settings_tab import SettingsTab
|
from gui.settings_tab import SettingsTab
|
||||||
|
|
||||||
|
|
||||||
@@ -29,6 +28,7 @@ class MainWindow:
|
|||||||
self.root = root
|
self.root = root
|
||||||
self.config = ConfigManager()
|
self.config = ConfigManager()
|
||||||
self.session_manager = session_manager
|
self.session_manager = session_manager
|
||||||
|
self.shared_production_ids = [] # 共享的 Production ID 列表
|
||||||
|
|
||||||
# 设置窗口属性(包含用户信息)
|
# 设置窗口属性(包含用户信息)
|
||||||
user_type_display = "管理员" if session_manager.is_admin() else "用户"
|
user_type_display = "管理员" if session_manager.is_admin() else "用户"
|
||||||
@@ -50,6 +50,18 @@ class MainWindow:
|
|||||||
# 居中窗口
|
# 居中窗口
|
||||||
self._center_window()
|
self._center_window()
|
||||||
|
|
||||||
|
def get_shared_production_ids(self) -> list:
|
||||||
|
"""获取共享的 Production ID 列表"""
|
||||||
|
return self.shared_production_ids.copy()
|
||||||
|
|
||||||
|
def update_shared_production_ids(self, production_ids: list):
|
||||||
|
"""更新共享的 Production ID 列表"""
|
||||||
|
self.shared_production_ids = production_ids
|
||||||
|
# 通知物料校验标签页 Production ID 已更新
|
||||||
|
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):
|
def create_menu(self):
|
||||||
"""创建菜单栏"""
|
"""创建菜单栏"""
|
||||||
menubar = tk.Menu(self.root)
|
menubar = tk.Menu(self.root)
|
||||||
@@ -71,22 +83,31 @@ class MainWindow:
|
|||||||
self.notebook = ttk.Notebook(self.root)
|
self.notebook = ttk.Notebook(self.root)
|
||||||
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
# 数据提取标签页
|
# 数据提取标签页(传递 self 以支持共享 Production ID)
|
||||||
self.extraction_tab = DataExtractionTab(self.notebook, self.config)
|
self.extraction_tab = DataExtractionTab(self.notebook, self.config, self)
|
||||||
self.notebook.add(self.extraction_tab, text="数据提取")
|
self.notebook.add(self.extraction_tab, text="数据提取")
|
||||||
|
|
||||||
# 物料校验标签页(传入 session_manager)
|
# 物料校验标签页(传入 session_manager 和 main_window)
|
||||||
self.validation_tab = MaterialValidationTab(self.notebook, self.config, self.session_manager)
|
self.validation_tab = MaterialValidationTab(self.notebook, self.config, self.session_manager, self)
|
||||||
self.notebook.add(self.validation_tab, text="物料校验")
|
self.notebook.add(self.validation_tab, text="物料校验")
|
||||||
|
|
||||||
# 数据查询标签页
|
|
||||||
self.query_tab = DataQueryTab(self.notebook, self.config)
|
|
||||||
self.notebook.add(self.query_tab, text="数据查询")
|
|
||||||
|
|
||||||
# 设置标签页(传入 session_manager)
|
# 设置标签页(传入 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="设置")
|
self.notebook.add(self.settings_tab, text="设置")
|
||||||
|
|
||||||
|
# 初始化:如果数据提取页面已有 Production ID,通知物料校验页面
|
||||||
|
self._initialize_shared_production_ids()
|
||||||
|
|
||||||
|
def _initialize_shared_production_ids(self):
|
||||||
|
"""初始化共享的 Production ID(从数据提取页面获取)"""
|
||||||
|
try:
|
||||||
|
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)
|
||||||
|
except Exception:
|
||||||
|
pass # 如果获取失败,忽略错误
|
||||||
|
|
||||||
def create_status_bar(self):
|
def create_status_bar(self):
|
||||||
"""创建状态栏"""
|
"""创建状态栏"""
|
||||||
self.status_bar = ttk.Frame(self.root, relief=tk.SUNKEN)
|
self.status_bar = ttk.Frame(self.root, relief=tk.SUNKEN)
|
||||||
@@ -145,8 +166,21 @@ class MainWindow:
|
|||||||
"功能:\n"
|
"功能:\n"
|
||||||
"• 数据提取 - 从 ERP 系统提取备料计划数据\n"
|
"• 数据提取 - 从 ERP 系统提取备料计划数据\n"
|
||||||
"• 物料校验 - 校验物料状态并匹配待删除物料\n"
|
"• 物料校验 - 校验物料状态并匹配待删除物料\n"
|
||||||
"• 数据查询 - 查询生产订单号等信息\n"
|
|
||||||
"• 设置管理 - 管理系统配置\n"
|
"• 设置管理 - 管理系统配置\n"
|
||||||
"• 数据库持久化 - 将提取的数据自动保存到 SQL Server\n\n"
|
"• 数据库持久化 - 将提取的数据自动保存到数据库\n\n"
|
||||||
"基于 Playwright 和 Python 开发",
|
"基于 Playwright 和 Python 开发",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def reload_config(self):
|
||||||
|
"""配置更新后重新加载配置到各个标签页"""
|
||||||
|
# 重新加载配置
|
||||||
|
self.config.reload()
|
||||||
|
|
||||||
|
# 通知各个标签页重新加载配置
|
||||||
|
if hasattr(self.extraction_tab, 'reload_config'):
|
||||||
|
self.extraction_tab.reload_config()
|
||||||
|
if hasattr(self.validation_tab, 'reload_config'):
|
||||||
|
self.validation_tab.reload_config()
|
||||||
|
|
||||||
|
# 更新状态栏
|
||||||
|
self.config_status.set("配置已重新加载")
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class CheckboxTreeview(ttk.Treeview):
|
|||||||
class MaterialValidationTab(ttk.Frame):
|
class MaterialValidationTab(ttk.Frame):
|
||||||
"""物料校验标签页"""
|
"""物料校验标签页"""
|
||||||
|
|
||||||
def __init__(self, parent, config: ConfigManager, session_manager):
|
def __init__(self, parent, config: ConfigManager, session_manager, main_window=None):
|
||||||
"""
|
"""
|
||||||
初始化物料校验标签页
|
初始化物料校验标签页
|
||||||
|
|
||||||
@@ -139,13 +139,16 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
parent: 父容器
|
parent: 父容器
|
||||||
config: 配置管理器
|
config: 配置管理器
|
||||||
session_manager: SessionManager 实例,用于权限控制
|
session_manager: SessionManager 实例,用于权限控制
|
||||||
|
main_window: 主窗口引用,用于获取共享的 Production ID
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.session_manager = session_manager
|
self.session_manager = session_manager
|
||||||
|
self.main_window = main_window
|
||||||
self.validating = False
|
self.validating = False
|
||||||
self.validation_results = None
|
self.validation_results = None
|
||||||
self.material_records_cache = None # 缓存完整的物料记录
|
self.material_records_cache = None # 缓存完整的物料记录
|
||||||
|
self.shared_production_ids = [] # 共享的 Production ID 列表
|
||||||
|
|
||||||
# 负责人筛选相关
|
# 负责人筛选相关
|
||||||
self.managers: List[str] = [] # 可用负责人列表
|
self.managers: List[str] = [] # 可用负责人列表
|
||||||
@@ -230,45 +233,46 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
# 普通用户:默认使用 database_filtered 模式
|
# 普通用户:默认使用 database_filtered 模式
|
||||||
self.source_mode = tk.StringVar(value="database_filtered")
|
self.source_mode = tk.StringVar(value="database_filtered")
|
||||||
|
|
||||||
# 文件选择
|
# Production ID 数据源(仅 Admin)
|
||||||
file_group = ttk.LabelFrame(parent, text="文件选择", padding=10)
|
if self.session_manager.is_admin():
|
||||||
file_group.pack(fill=tk.X, pady=5)
|
source_frame = ttk.Frame(parent)
|
||||||
|
source_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
# 数据库全表模式 - 无需输入文件
|
# Production ID 数据源选择
|
||||||
self.db_full_frame = ttk.Frame(file_group)
|
self.production_id_source_var = tk.StringVar(value="shared")
|
||||||
ttk.Label(
|
ttk.Radiobutton(
|
||||||
self.db_full_frame,
|
source_frame,
|
||||||
text="数据库全表模式:将查询 DiscreteMaterialPlanData 表中的所有材料",
|
text="使用文件:",
|
||||||
foreground="gray"
|
variable=self.production_id_source_var,
|
||||||
).pack(anchor="w")
|
value="file",
|
||||||
|
command=self._on_production_id_source_changed
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
# 数据库过滤模式 - 需要 ProductionID 文件
|
ttk.Radiobutton(
|
||||||
self.db_filtered_frame = ttk.Frame(file_group)
|
source_frame,
|
||||||
self.db_filtered_production_id_selector = FileSelector(
|
text="使用数据提取页面的 Production ID:",
|
||||||
self.db_filtered_frame,
|
variable=self.production_id_source_var,
|
||||||
label_text="ProductionID 文件:",
|
value="shared",
|
||||||
file_type="file",
|
command=self._on_production_id_source_changed
|
||||||
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
).pack(side=tk.LEFT, padx=10)
|
||||||
initial_dir="D:/python/playwrite/",
|
|
||||||
)
|
|
||||||
self.db_filtered_production_id_selector.pack(fill=tk.X)
|
|
||||||
|
|
||||||
# 输出文件(对于所有用户都可见)
|
# 文件选择器
|
||||||
self.output_file_selector = FileSelector(
|
self.db_filtered_production_id_selector = FileSelector(
|
||||||
file_group,
|
parent,
|
||||||
label_text="输出文件:",
|
label_text="",
|
||||||
file_type="file",
|
file_type="file",
|
||||||
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
||||||
initial_dir=self.config.get("paths.data_dir", "data/"),
|
initial_dir="D:/python/playwrite/",
|
||||||
)
|
)
|
||||||
self.output_file_selector.pack(fill=tk.X, pady=(10, 0))
|
self.db_filtered_production_id_selector.pack(fill=tk.X)
|
||||||
|
|
||||||
# 设置默认输出
|
# 共享 Production ID 提示标签
|
||||||
default_output = os.path.join(
|
self.shared_ids_info_label = ttk.Label(
|
||||||
self.config.get("paths.data_dir", "data/"),
|
parent,
|
||||||
self.config.get("paths.validation_output", "物料状态校验结果.xlsx"),
|
text="",
|
||||||
)
|
foreground="blue"
|
||||||
self.output_file_selector.set(default_output)
|
)
|
||||||
|
# 初始时不显示
|
||||||
|
|
||||||
# 控制按钮
|
# 控制按钮
|
||||||
button_frame = ttk.Frame(parent)
|
button_frame = ttk.Frame(parent)
|
||||||
@@ -377,24 +381,71 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
|
|
||||||
def _on_source_mode_change(self):
|
def _on_source_mode_change(self):
|
||||||
"""数据源模式切换"""
|
"""数据源模式切换"""
|
||||||
# PERMISSION CHECK: 普通用户固定为 database_filtered 模式
|
# PERMISSION CHECK: 仅管理员用户可以选择数据源模式
|
||||||
if not self.session_manager.is_admin():
|
if not self.session_manager.is_admin():
|
||||||
# 对于普通用户,pack 数据库过滤模式框架
|
|
||||||
self.db_full_frame.pack_forget()
|
|
||||||
self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
mode = self.source_mode.get()
|
mode = self.source_mode.get()
|
||||||
|
|
||||||
# 隐藏所有文件选择框架
|
# 根据模式显示 Production ID 数据源选择(仅 Admin)
|
||||||
self.db_full_frame.pack_forget()
|
|
||||||
self.db_filtered_frame.pack_forget()
|
|
||||||
|
|
||||||
# 根据模式显示对应的文件选择器
|
|
||||||
if mode == "database_full":
|
if mode == "database_full":
|
||||||
self.db_full_frame.pack(fill=tk.X, pady=(0, 5))
|
# 全表模式:隐藏 Production ID 选择区域
|
||||||
|
if hasattr(self, 'source_frame'):
|
||||||
|
self.source_frame.pack_forget()
|
||||||
elif mode == "database_filtered":
|
elif mode == "database_filtered":
|
||||||
self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5))
|
# 过滤模式:显示 Production ID 选择区域
|
||||||
|
if hasattr(self, 'source_frame'):
|
||||||
|
self.source_frame.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
|
def _on_production_id_source_changed(self):
|
||||||
|
"""Production ID 数据源切换回调(仅 Admin 模式)"""
|
||||||
|
# User 模式下没有文件选择器,直接返回
|
||||||
|
if not self.session_manager.is_admin():
|
||||||
|
return
|
||||||
|
|
||||||
|
source = self.production_id_source_var.get()
|
||||||
|
if source == "shared":
|
||||||
|
# 使用共享 Production ID
|
||||||
|
self.db_filtered_production_id_selector.pack_forget()
|
||||||
|
self.shared_ids_info_label.pack(anchor="w", pady=(0, 5))
|
||||||
|
else:
|
||||||
|
# 使用文件
|
||||||
|
self.shared_ids_info_label.pack_forget()
|
||||||
|
self.db_filtered_production_id_selector.pack(fill=tk.X)
|
||||||
|
|
||||||
|
def on_production_ids_updated(self, production_ids: list):
|
||||||
|
"""当数据提取页面的 Production ID 更新时调用"""
|
||||||
|
self.shared_production_ids = production_ids
|
||||||
|
|
||||||
|
is_user_only = not self.session_manager.is_admin()
|
||||||
|
|
||||||
|
# User 模式:静默更新,不显示任何提示
|
||||||
|
if is_user_only:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Admin 模式:显示提示信息
|
||||||
|
if production_ids:
|
||||||
|
count = len(production_ids)
|
||||||
|
preview = ", ".join(production_ids[:3])
|
||||||
|
if count > 3:
|
||||||
|
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():
|
||||||
|
self.shared_ids_label.pack(anchor="w", pady=(0, 5))
|
||||||
|
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()
|
||||||
|
|
||||||
|
def reload_config(self):
|
||||||
|
"""配置更新后重新加载"""
|
||||||
|
# 从主窗口获取最新的 Production ID
|
||||||
|
if self.main_window:
|
||||||
|
production_ids = self.main_window.get_shared_production_ids()
|
||||||
|
self.on_production_ids_updated(production_ids)
|
||||||
|
|
||||||
def _create_manager_filter_area(self, parent):
|
def _create_manager_filter_area(self, parent):
|
||||||
"""创建负责人筛选区域"""
|
"""创建负责人筛选区域"""
|
||||||
@@ -707,28 +758,53 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
|
|
||||||
def start_validation(self):
|
def start_validation(self):
|
||||||
"""开始校验"""
|
"""开始校验"""
|
||||||
# 验证输入
|
# 从配置获取输出文件路径
|
||||||
output_file = self.output_file_selector.get()
|
output_file = os.path.join(
|
||||||
|
self.config.get("paths.data_dir", "data/"),
|
||||||
|
self.config.get("paths.validation_output", "物料状态校验结果.xlsx"),
|
||||||
|
)
|
||||||
if not output_file:
|
if not output_file:
|
||||||
messagebox.showerror("错误", "请指定输出文件路径")
|
messagebox.showerror("错误", "请先在设置页面配置输出文件路径")
|
||||||
return
|
return
|
||||||
|
|
||||||
mode = self.source_mode.get()
|
mode = self.source_mode.get()
|
||||||
input_file = None
|
input_file = None
|
||||||
production_id_file = None
|
production_id_file = None
|
||||||
|
production_ids_list = None # 新增:用于传递共享的 Production ID 列表
|
||||||
|
|
||||||
|
is_user_only = not self.session_manager.is_admin()
|
||||||
|
|
||||||
# 根据模式验证输入文件
|
# 根据模式验证输入文件
|
||||||
if mode == "database_full":
|
if mode == "database_full":
|
||||||
# 无需输入文件
|
# 无需输入文件
|
||||||
pass
|
pass
|
||||||
elif mode == "database_filtered":
|
elif mode == "database_filtered":
|
||||||
production_id_file = self.db_filtered_production_id_selector.get()
|
if is_user_only:
|
||||||
if not production_id_file:
|
# User 模式:自动使用共享的 Production ID
|
||||||
messagebox.showerror("错误", "请选择 ProductionID 文件")
|
production_ids_list = self.shared_production_ids
|
||||||
return
|
if not production_ids_list:
|
||||||
if not os.path.exists(production_id_file):
|
messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试")
|
||||||
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
|
return
|
||||||
return
|
self.log_text.info(f"使用数据提取页面的 Production ID({len(production_ids_list)} 个)")
|
||||||
|
else:
|
||||||
|
# Admin 模式:检查数据源选择
|
||||||
|
source = self.production_id_source_var.get()
|
||||||
|
if source == "shared":
|
||||||
|
# 使用共享的 Production ID
|
||||||
|
production_ids_list = self.shared_production_ids
|
||||||
|
if not production_ids_list:
|
||||||
|
messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试")
|
||||||
|
return
|
||||||
|
self.log_text.info(f"使用共享的 Production ID({len(production_ids_list)} 个)")
|
||||||
|
else:
|
||||||
|
# 使用文件
|
||||||
|
production_id_file = self.db_filtered_production_id_selector.get()
|
||||||
|
if not production_id_file:
|
||||||
|
messagebox.showerror("错误", "请选择 ProductionID 文件")
|
||||||
|
return
|
||||||
|
if not os.path.exists(production_id_file):
|
||||||
|
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("错误", f"未知的校验模式: {mode}")
|
messagebox.showerror("错误", f"未知的校验模式: {mode}")
|
||||||
return
|
return
|
||||||
@@ -758,16 +834,27 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
# 在后台线程中执行校验
|
# 在后台线程中执行校验
|
||||||
validation_thread = threading.Thread(
|
validation_thread = threading.Thread(
|
||||||
target=self._validation_worker_enhanced,
|
target=self._validation_worker_enhanced,
|
||||||
args=(mode, input_file, production_id_file, output_file),
|
args=(mode, input_file, production_id_file, output_file, production_ids_list),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
validation_thread.start()
|
validation_thread.start()
|
||||||
|
|
||||||
def _validation_worker_enhanced(
|
def _validation_worker_enhanced(
|
||||||
self, mode: str, input_file: str, production_id_file: str, output_file: str
|
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:
|
try:
|
||||||
|
# 如果提供了共享的 Production ID 列表,创建临时文件
|
||||||
|
if production_ids_list:
|
||||||
|
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))
|
||||||
|
production_id_file = temp_production_id_file
|
||||||
|
self._update_log(f"使用共享的 Production ID 列表({len(production_ids_list)} 个)", "INFO")
|
||||||
|
|
||||||
# 导入校验器
|
# 导入校验器
|
||||||
from utils.material_status_validator import MaterialStatusValidator
|
from utils.material_status_validator import MaterialStatusValidator
|
||||||
|
|
||||||
@@ -820,6 +907,12 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
import traceback
|
import traceback
|
||||||
self._update_log(traceback.format_exc(), "ERROR")
|
self._update_log(traceback.format_exc(), "ERROR")
|
||||||
finally:
|
finally:
|
||||||
|
# 清理临时文件
|
||||||
|
if temp_production_id_file and os.path.exists(temp_production_id_file):
|
||||||
|
try:
|
||||||
|
os.unlink(temp_production_id_file)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
# 更新 UI 状态
|
# 更新 UI 状态
|
||||||
self.after(0, self._validation_complete)
|
self.after(0, self._validation_complete)
|
||||||
|
|
||||||
@@ -1082,17 +1175,29 @@ class MaterialValidationTab(ttk.Frame):
|
|||||||
|
|
||||||
def export_results(self):
|
def export_results(self):
|
||||||
"""导出结果到 Excel"""
|
"""导出结果到 Excel"""
|
||||||
output_file = self.output_file_selector.get()
|
# 从配置获取输出文件路径
|
||||||
|
data_dir = self.config.get("paths.data_dir", "data/")
|
||||||
|
validation_filename = self.config.get("paths.validation_output", "物料状态校验结果.xlsx")
|
||||||
|
output_file = os.path.join(data_dir, validation_filename)
|
||||||
|
|
||||||
if not output_file:
|
# 如果配置的文件不存在,提示用户选择位置
|
||||||
output_file = filedialog.asksaveasfilename(
|
if not os.path.exists(data_dir):
|
||||||
title="保存结果",
|
output_dir = filedialog.askdirectory(
|
||||||
defaultextension=".xlsx",
|
title="选择输出目录",
|
||||||
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
initialdir=data_dir
|
||||||
)
|
)
|
||||||
|
if output_dir:
|
||||||
|
self.config.set("paths.data_dir", output_dir)
|
||||||
|
# 重新读取配置并构建输出文件路径
|
||||||
|
data_dir = output_dir
|
||||||
|
output_file = os.path.join(data_dir, validation_filename)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
if not output_file:
|
# 如果文件已存在,提示用户覆盖
|
||||||
return
|
if os.path.exists(output_file):
|
||||||
|
if not messagebox.askyesno("确认覆盖", f"文件已存在:{output_file}\n是否覆盖?"):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 收集表格数据
|
# 收集表格数据
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class SettingsTab(ttk.Frame):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.config = config
|
self.config = config
|
||||||
self.session_manager = session_manager
|
self.session_manager = session_manager
|
||||||
|
|
||||||
|
# 初始化物料校验输出文件名变量
|
||||||
|
self.validation_output_filename_var = tk.StringVar()
|
||||||
|
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
self.load_settings()
|
self.load_settings()
|
||||||
|
|
||||||
@@ -57,19 +61,26 @@ class SettingsTab(ttk.Frame):
|
|||||||
self._create_paths_group(scrollable_frame)
|
self._create_paths_group(scrollable_frame)
|
||||||
self._create_extraction_group(scrollable_frame)
|
self._create_extraction_group(scrollable_frame)
|
||||||
self._create_validation_group(scrollable_frame)
|
self._create_validation_group(scrollable_frame)
|
||||||
|
self._create_ui_group(scrollable_frame)
|
||||||
|
else:
|
||||||
|
# User 用户:只显示路径配置
|
||||||
|
self._create_paths_group(scrollable_frame)
|
||||||
|
|
||||||
# 按钮区域 - 根据用户类型显示不同按钮
|
# 按钮区域 - 根据用户类型显示不同按钮
|
||||||
button_frame = ttk.Frame(scrollable_frame)
|
button_frame = ttk.Frame(scrollable_frame)
|
||||||
button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew")
|
button_frame.grid(row=7, column=0, columnspan=2, pady=20, sticky="ew")
|
||||||
|
|
||||||
if is_user_only:
|
if is_user_only:
|
||||||
# 普通用户只显示测试按钮
|
# User 用户:显示测试按钮和保存设置按钮
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
|
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
|
||||||
).pack(side="left", padx=5)
|
).pack(side="left", padx=5)
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
button_frame, text="测试数据库连接", command=self.test_db_connection
|
button_frame, text="测试数据库连接", command=self.test_db_connection
|
||||||
).pack(side="left", padx=5)
|
).pack(side="left", padx=5)
|
||||||
|
ttk.Button(button_frame, text="保存设置", command=self.save_settings).pack(
|
||||||
|
side="left", padx=5
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 管理员显示所有按钮
|
# 管理员显示所有按钮
|
||||||
ttk.Button(
|
ttk.Button(
|
||||||
@@ -233,7 +244,13 @@ class SettingsTab(ttk.Frame):
|
|||||||
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.default_output_var = tk.StringVar()
|
self.default_output_var = tk.StringVar()
|
||||||
ttk.Entry(group, textvariable=self.default_output_var, width=40).grid(
|
ttk.Entry(group, textvariable=self.default_output_var, width=40).grid(
|
||||||
row=3, column=0, columnspan=2, sticky="ew", pady=5
|
row=2, column=1, columnspan=2, sticky="ew", pady=5
|
||||||
|
)
|
||||||
|
|
||||||
|
# 校验输出文件
|
||||||
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
group.columnconfigure(0, weight=1)
|
group.columnconfigure(0, weight=1)
|
||||||
@@ -297,15 +314,21 @@ class SettingsTab(ttk.Frame):
|
|||||||
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)
|
).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.Label(group, text="数据库批次大小:").grid(row=2, 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)
|
self.validation_batch_size_var = tk.IntVar(value=2000)
|
||||||
ttk.Spinbox(
|
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=2, column=1, sticky="w", pady=5)
|
).grid(row=3, 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=4, column=0, sticky="w", pady=5)
|
||||||
self.validation_match_mode_var = tk.StringVar()
|
self.validation_match_mode_var = tk.StringVar()
|
||||||
match_mode_combo = ttk.Combobox(
|
match_mode_combo = ttk.Combobox(
|
||||||
group,
|
group,
|
||||||
@@ -314,31 +337,67 @@ class SettingsTab(ttk.Frame):
|
|||||||
state="readonly",
|
state="readonly",
|
||||||
width=30,
|
width=30,
|
||||||
)
|
)
|
||||||
match_mode_combo.grid(row=3, column=1, sticky="w", pady=5)
|
match_mode_combo.grid(row=4, column=1, sticky="w", pady=5)
|
||||||
|
|
||||||
# CRUD 操作
|
# CRUD 操作
|
||||||
self.validation_enable_crud_var = tk.BooleanVar()
|
self.validation_enable_crud_var = tk.BooleanVar()
|
||||||
ttk.Checkbutton(
|
ttk.Checkbutton(
|
||||||
group, text="启用 CRUD 操作(管理待删除物料)", variable=self.validation_enable_crud_var
|
group, text="启用 CRUD 操作(管理待删除物料)", variable=self.validation_enable_crud_var
|
||||||
).grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
|
).grid(row=5, column=0, columnspan=2, sticky="w", pady=5)
|
||||||
|
|
||||||
# 默认负责人
|
# 默认负责人
|
||||||
ttk.Label(group, text="默认负责人:").grid(row=5, column=0, sticky="w", pady=5)
|
ttk.Label(group, text="默认负责人:").grid(row=6, column=0, sticky="w", pady=5)
|
||||||
self.validation_default_manager_var = tk.StringVar()
|
self.validation_default_manager_var = tk.StringVar()
|
||||||
ttk.Entry(group, textvariable=self.validation_default_manager_var, width=30).grid(
|
ttk.Entry(group, textvariable=self.validation_default_manager_var, width=30).grid(
|
||||||
row=5, column=1, sticky="w", pady=5
|
row=6, column=1, sticky="w", pady=5
|
||||||
)
|
)
|
||||||
|
|
||||||
group.columnconfigure(1, weight=1)
|
group.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
def _create_ui_group(self, parent):
|
||||||
|
"""创建 UI 配置组"""
|
||||||
|
group = ttk.LabelFrame(parent, text="界面设置", padding=10)
|
||||||
|
group.grid(row=5, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
|
||||||
|
|
||||||
|
# 字体选择
|
||||||
|
ttk.Label(group, text="字体:").grid(row=0, column=0, sticky="w", pady=5)
|
||||||
|
self.ui_font_family_var = tk.StringVar()
|
||||||
|
font_combo = ttk.Combobox(
|
||||||
|
group,
|
||||||
|
textvariable=self.ui_font_family_var,
|
||||||
|
values=["Microsoft YaHei UI", "SimSun", "KaiTi", "FangSong", "Arial", "Segoe UI"],
|
||||||
|
state="readonly",
|
||||||
|
width=30,
|
||||||
|
)
|
||||||
|
font_combo.grid(row=0, column=1, sticky="w", pady=5)
|
||||||
|
|
||||||
|
# 字号选择
|
||||||
|
ttk.Label(group, text="字号:").grid(row=1, column=0, sticky="w", pady=5)
|
||||||
|
self.ui_font_size_var = tk.IntVar(value=10)
|
||||||
|
ttk.Spinbox(
|
||||||
|
group, from_=8, to=24, textvariable=self.ui_font_size_var, width=10
|
||||||
|
).grid(row=1, column=1, sticky="w", pady=5)
|
||||||
|
|
||||||
|
# Production ID 输入框宽度
|
||||||
|
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
|
||||||
|
).grid(row=2, column=1, sticky="w", pady=5)
|
||||||
|
|
||||||
|
group.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
def load_settings(self):
|
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:
|
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", ""))
|
||||||
return
|
return
|
||||||
|
|
||||||
# 管理员模式 - 加载所有配置
|
# 管理员模式 - 加载所有配置
|
||||||
@@ -386,13 +445,38 @@ class SettingsTab(ttk.Frame):
|
|||||||
# 校验设置
|
# 校验设置
|
||||||
self.validation_data_source_var.set(self.config.get("validation.data_source", "database_full"))
|
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_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_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_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_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_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_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):
|
def save_settings(self):
|
||||||
"""保存界面设置到配置"""
|
"""保存界面设置到配置"""
|
||||||
|
# 判断是否为仅测试用户模式
|
||||||
|
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())
|
||||||
|
|
||||||
|
# 保存到文件
|
||||||
|
if self.config.save():
|
||||||
|
messagebox.showinfo("成功", "设置已保存")
|
||||||
|
# 通知其他标签页重新加载配置
|
||||||
|
self._notify_config_reload()
|
||||||
|
else:
|
||||||
|
messagebox.showerror("错误", "保存设置失败")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 管理员模式 - 加载所有配置
|
||||||
# ERP 设置
|
# ERP 设置
|
||||||
self.config.set("erp.url", self.erp_url_var.get())
|
self.config.set("erp.url", self.erp_url_var.get())
|
||||||
self.config.set("erp.username", self.erp_username_var.get())
|
self.config.set("erp.username", self.erp_username_var.get())
|
||||||
@@ -434,17 +518,37 @@ class SettingsTab(ttk.Frame):
|
|||||||
# 校验设置
|
# 校验设置
|
||||||
self.config.set("validation.data_source", self.validation_data_source_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("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.batch_size", self.validation_batch_size_var.get())
|
||||||
self.config.set("validation.match_mode", self.validation_match_mode_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.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.default_manager", self.validation_default_manager_var.get())
|
||||||
|
|
||||||
|
# UI 设置
|
||||||
|
self.config.set("ui.font_family", self.ui_font_family_var.get())
|
||||||
|
self.config.set("ui.font_size", self.ui_font_size_var.get())
|
||||||
|
self.config.set("ui.production_id_input_width", self.ui_input_width_var.get())
|
||||||
|
|
||||||
# 保存到文件
|
# 保存到文件
|
||||||
if self.config.save():
|
if self.config.save():
|
||||||
messagebox.showinfo("成功", "设置已保存")
|
messagebox.showinfo("成功", "设置已保存")
|
||||||
|
# 通知其他标签页重新加载配置
|
||||||
|
self._notify_config_reload()
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("错误", "保存设置失败")
|
messagebox.showerror("错误", "保存设置失败")
|
||||||
|
|
||||||
|
def _notify_config_reload(self):
|
||||||
|
"""通知其他标签页配置已更新"""
|
||||||
|
# 尝试通知主窗口重新加载配置
|
||||||
|
try:
|
||||||
|
# 获取主窗口
|
||||||
|
main_window = self.winfo_toplevel()
|
||||||
|
# 调用主窗口的 reload_config 方法(如果存在)
|
||||||
|
if hasattr(main_window, 'reload_config'):
|
||||||
|
main_window.reload_config()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def test_db_connection(self):
|
def test_db_connection(self):
|
||||||
"""测试数据库连接"""
|
"""测试数据库连接"""
|
||||||
# 从配置读取而不是从 UI 变量(支持 User 类型用户)
|
# 从配置读取而不是从 UI 变量(支持 User 类型用户)
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ GUI 自定义组件模块
|
|||||||
|
|
||||||
from .file_selector import FileSelector
|
from .file_selector import FileSelector
|
||||||
from .log_text import LogText
|
from .log_text import LogText
|
||||||
|
from .production_id_input import ProductionIdInput
|
||||||
|
|
||||||
__all__ = ['FileSelector', 'LogText']
|
__all__ = ['FileSelector', 'LogText', 'ProductionIdInput']
|
||||||
|
|||||||
@@ -178,3 +178,9 @@ class LogText(tk.Frame):
|
|||||||
def grid(self, **kwargs):
|
def grid(self, **kwargs):
|
||||||
"""Grid 布局"""
|
"""Grid 布局"""
|
||||||
super().grid(**kwargs)
|
super().grid(**kwargs)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
187
gui/widgets/production_id_input.py
Normal file
187
gui/widgets/production_id_input.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Production ID 输入控件
|
||||||
|
|
||||||
|
多行文本输入框,用于输入 Production ID 列表。
|
||||||
|
支持内容溢出时自动显示滚动条,左侧显示行号。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, font
|
||||||
|
from tklinenums import TkLineNumbers
|
||||||
|
|
||||||
|
|
||||||
|
class ProductionIdInput(ttk.Frame):
|
||||||
|
"""Production ID 输入控件,带行号和自动滚动条"""
|
||||||
|
|
||||||
|
def __init__(self, parent, placeholder="每行输入一个 Production ID", **kwargs):
|
||||||
|
super().__init__(parent, **kwargs)
|
||||||
|
self.placeholder = placeholder
|
||||||
|
self._scrollbar_visible = False
|
||||||
|
self._check_pending = False
|
||||||
|
self._updating_placeholder = False
|
||||||
|
|
||||||
|
# 创建文本框
|
||||||
|
self.text_widget = tk.Text(self, wrap=tk.WORD, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 创建行号区域的容器
|
||||||
|
self.linenums_frame = ttk.Frame(self)
|
||||||
|
|
||||||
|
# 创建行号控件(使用黑色)
|
||||||
|
self.line_numbers = TkLineNumbers(
|
||||||
|
self.linenums_frame,
|
||||||
|
self.text_widget,
|
||||||
|
justify="center",
|
||||||
|
colors=("black", "#f0f0f0"),
|
||||||
|
bg="#f0f0f0",
|
||||||
|
width=3
|
||||||
|
)
|
||||||
|
self.line_numbers.pack(fill="both", expand=True)
|
||||||
|
|
||||||
|
# 创建滚动条
|
||||||
|
self.scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.text_widget.yview)
|
||||||
|
self.text_widget.configure(yscrollcommand=self._on_scroll)
|
||||||
|
|
||||||
|
# 布局:行号 | 文本框 | 滚动条
|
||||||
|
self.linenums_frame.grid(row=0, column=0, sticky="ns")
|
||||||
|
self.text_widget.grid(row=0, column=1, sticky="nsew")
|
||||||
|
self.scrollbar.grid(row=0, column=2, sticky="ns")
|
||||||
|
|
||||||
|
# 配置行列权重
|
||||||
|
self.grid_rowconfigure(0, weight=1)
|
||||||
|
self.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# 绑定事件
|
||||||
|
self.text_widget.bind("<FocusIn>", self._on_focus_in)
|
||||||
|
self.text_widget.bind("<FocusOut>", self._on_focus_out)
|
||||||
|
# 监听内容变化事件
|
||||||
|
self.text_widget.bind("<KeyRelease>", self._on_content_change)
|
||||||
|
self.text_widget.bind("<ButtonRelease-1>", self._schedule_check)
|
||||||
|
self.text_widget.bind("<ButtonRelease-3>", self._schedule_check)
|
||||||
|
self.text_widget.bind("<Configure>", self._schedule_check)
|
||||||
|
# 绑定粘贴事件
|
||||||
|
self.text_widget.bind("<<Paste>>", self._schedule_check)
|
||||||
|
|
||||||
|
# 初始隐藏滚动条(行号始终显示)
|
||||||
|
self.scrollbar.grid_remove()
|
||||||
|
|
||||||
|
# 显示占位符
|
||||||
|
self._show_placeholder()
|
||||||
|
|
||||||
|
# 延迟检查初始状态
|
||||||
|
self.after(100, self._check_ui_state)
|
||||||
|
|
||||||
|
def _on_focus_in(self, event):
|
||||||
|
"""获得焦点时隐藏占位符"""
|
||||||
|
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")
|
||||||
|
|
||||||
|
def _on_focus_out(self, event):
|
||||||
|
"""失去焦点时显示占位符"""
|
||||||
|
content = self.text_widget.get("1.0", "end-1c")
|
||||||
|
if not content:
|
||||||
|
self._show_placeholder()
|
||||||
|
|
||||||
|
def _show_placeholder(self):
|
||||||
|
"""显示占位符"""
|
||||||
|
self._updating_placeholder = True
|
||||||
|
self.text_widget.delete("1.0", tk.END)
|
||||||
|
self.text_widget.insert("1.0", self.placeholder)
|
||||||
|
self.text_widget.configure(foreground="gray")
|
||||||
|
self._updating_placeholder = False
|
||||||
|
self._schedule_check()
|
||||||
|
|
||||||
|
def _hide_placeholder(self):
|
||||||
|
"""隐藏占位符"""
|
||||||
|
self._updating_placeholder = True
|
||||||
|
if self.text_widget.get("1.0", "end-1c") == self.placeholder:
|
||||||
|
self.text_widget.delete("1.0", tk.END)
|
||||||
|
self.text_widget.configure(foreground="black")
|
||||||
|
self._updating_placeholder = False
|
||||||
|
|
||||||
|
def _on_content_change(self, event=None):
|
||||||
|
"""内容变化时的处理"""
|
||||||
|
# 如果不是占位符状态,重绘行号
|
||||||
|
if self.text_widget.get("1.0", "end-1c") != self.placeholder:
|
||||||
|
self.line_numbers.redraw()
|
||||||
|
self._schedule_check()
|
||||||
|
|
||||||
|
def get(self) -> list[str]:
|
||||||
|
"""获取 Production ID 列表"""
|
||||||
|
self._hide_placeholder()
|
||||||
|
content = self.text_widget.get("1.0", "end-1c").strip()
|
||||||
|
return [line.strip() for line in content.split("\n") if line.strip()]
|
||||||
|
|
||||||
|
def set(self, production_ids: list[str]):
|
||||||
|
"""设置 Production ID 列表"""
|
||||||
|
self._updating_placeholder = True
|
||||||
|
self.text_widget.delete("1.0", tk.END)
|
||||||
|
if production_ids:
|
||||||
|
self.text_widget.insert("1.0", "\n".join(production_ids))
|
||||||
|
self.text_widget.configure(foreground="black")
|
||||||
|
self.after_idle(self.line_numbers.redraw)
|
||||||
|
else:
|
||||||
|
self._show_placeholder()
|
||||||
|
self._updating_placeholder = False
|
||||||
|
self._schedule_check()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""清空内容"""
|
||||||
|
self._updating_placeholder = True
|
||||||
|
self.text_widget.delete("1.0", tk.END)
|
||||||
|
self._show_placeholder()
|
||||||
|
self._updating_placeholder = False
|
||||||
|
self._schedule_check()
|
||||||
|
|
||||||
|
def append(self, production_ids: list[str]):
|
||||||
|
"""追加 Production ID 列表"""
|
||||||
|
self._hide_placeholder()
|
||||||
|
if production_ids:
|
||||||
|
current_content = self.text_widget.get("1.0", "end-1c")
|
||||||
|
if current_content.strip():
|
||||||
|
self.text_widget.insert(tk.END, "\n" + "\n".join(production_ids))
|
||||||
|
else:
|
||||||
|
self.text_widget.insert("1.0", "\n".join(production_ids))
|
||||||
|
self.after_idle(self.line_numbers.redraw)
|
||||||
|
self._schedule_check()
|
||||||
|
|
||||||
|
def apply_font(self, font_family: str, font_size: int):
|
||||||
|
"""应用字体设置"""
|
||||||
|
font_spec = font.Font(family=font_family, size=font_size)
|
||||||
|
self.text_widget.configure(font=font_spec)
|
||||||
|
# 重绘行号以应用字体变化
|
||||||
|
self.after_idle(self.line_numbers.redraw)
|
||||||
|
|
||||||
|
def _on_scroll(self, first, last):
|
||||||
|
"""滚动回调,更新滚动条位置和行号"""
|
||||||
|
self.scrollbar.set(first, last)
|
||||||
|
# 滚动时重绘行号以同步显示
|
||||||
|
self.line_numbers.redraw()
|
||||||
|
|
||||||
|
def _schedule_check(self, event=None):
|
||||||
|
"""调度 UI 状态检查(防抖)"""
|
||||||
|
if not self._check_pending:
|
||||||
|
self._check_pending = True
|
||||||
|
self.after(50, self._check_ui_state)
|
||||||
|
|
||||||
|
def _check_ui_state(self):
|
||||||
|
"""检查 UI 状态(滚动条)"""
|
||||||
|
self._check_pending = False
|
||||||
|
|
||||||
|
# 更新界面以确保获取准确的尺寸
|
||||||
|
self.text_widget.update_idletasks()
|
||||||
|
|
||||||
|
# 检查是否需要显示滚动条
|
||||||
|
first, last = self.text_widget.yview()
|
||||||
|
needs_scrollbar = last < 1.0
|
||||||
|
|
||||||
|
if needs_scrollbar != self._scrollbar_visible:
|
||||||
|
if needs_scrollbar:
|
||||||
|
self.scrollbar.grid()
|
||||||
|
self._scrollbar_visible = True
|
||||||
|
else:
|
||||||
|
self.scrollbar.grid_remove()
|
||||||
|
self._scrollbar_visible = False
|
||||||
@@ -19,3 +19,4 @@ numpy>=1.24.0
|
|||||||
python-dateutil>=2.8.0
|
python-dateutil>=2.8.0
|
||||||
pytz>=2023.0
|
pytz>=2023.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
|
tklinenums>=1.7.0
|
||||||
|
|||||||
@@ -1,19 +1,41 @@
|
|||||||
"""
|
"""
|
||||||
离散备料计划维护数据提取工具
|
离散备料计划维护数据提取工具 - 日志同步优化版
|
||||||
负责登录、批量下载、转换数据
|
功能:负责登录 ERP、批量下载数据、转换并合并数据,支持与 UI 实时同步标准格式日志。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from playwright.sync_api import sync_playwright
|
from typing import Callable, Optional, List
|
||||||
|
from playwright.sync_api import sync_playwright, TimeoutError
|
||||||
|
|
||||||
|
# 统一顶部导入
|
||||||
from utils.excel_converter import ExcelConverter
|
from utils.excel_converter import ExcelConverter
|
||||||
from utils.auth import login, logout
|
from utils.auth import login, logout
|
||||||
from db.production_order_query import (
|
from db.production_order_query import (
|
||||||
read_production_ids,
|
read_production_ids,
|
||||||
query_production_order_numbers,
|
query_production_order_numbers,
|
||||||
)
|
)
|
||||||
from typing import Callable, Optional
|
|
||||||
|
|
||||||
|
# --- 进度条对象导入 (保持容错) ---
|
||||||
|
try:
|
||||||
|
from gui.progress import ProgressInfo
|
||||||
|
except ImportError:
|
||||||
|
ProgressInfo = None
|
||||||
|
|
||||||
|
# --- 全局日志配置 ---
|
||||||
|
# 调整格式:增加 [] 使其与 UI 控件的默认风格保持一致
|
||||||
|
LOG_FORMAT = '[%(asctime)s] [%(levelname)s] %(message)s'
|
||||||
|
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format=LOG_FORMAT,
|
||||||
|
datefmt=DATE_FORMAT
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class DiscreteMaterialPlanExtractor:
|
class DiscreteMaterialPlanExtractor:
|
||||||
"""离散备料计划维护数据提取器"""
|
"""离散备料计划维护数据提取器"""
|
||||||
@@ -22,17 +44,6 @@ class DiscreteMaterialPlanExtractor:
|
|||||||
self, username, password, headless=False, verbose=True, batch_size=100,
|
self, username, password, headless=False, verbose=True, batch_size=100,
|
||||||
enable_db_persistence=False
|
enable_db_persistence=False
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
初始化提取器
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: 登录用户名
|
|
||||||
password: 登录密码
|
|
||||||
headless: 是否无头模式运行
|
|
||||||
verbose: 是否打印详细日志
|
|
||||||
batch_size: 批次大小
|
|
||||||
enable_db_persistence: 是否启用数据库持久化
|
|
||||||
"""
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
@@ -42,33 +53,37 @@ class DiscreteMaterialPlanExtractor:
|
|||||||
self.converter = ExcelConverter(verbose=verbose)
|
self.converter = ExcelConverter(verbose=verbose)
|
||||||
self.enable_db_persistence = enable_db_persistence
|
self.enable_db_persistence = enable_db_persistence
|
||||||
self.dao = None
|
self.dao = None
|
||||||
|
|
||||||
if self.enable_db_persistence:
|
if self.enable_db_persistence:
|
||||||
from db.discrete_material_plan_dao import DiscreteMaterialPlanDAO
|
|
||||||
self.dao = DiscreteMaterialPlanDAO()
|
|
||||||
self.dao.__enter__() # Enter context manager
|
|
||||||
|
|
||||||
def _print(self, *args, **kwargs):
|
|
||||||
"""打印日志(如果 verbose=True)"""
|
|
||||||
if self.verbose:
|
|
||||||
print(*args, **kwargs)
|
|
||||||
|
|
||||||
def _report_progress(
|
|
||||||
self, stage: str, current: int, total: int, message: str, **detail
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
报告进度
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stage: 阶段标识
|
|
||||||
current: 当前进度值
|
|
||||||
total: 总量
|
|
||||||
message: 显示消息
|
|
||||||
**detail: 额外详细信息
|
|
||||||
"""
|
|
||||||
if self.progress_callback:
|
|
||||||
try:
|
try:
|
||||||
from gui.progress import ProgressInfo
|
from db.discrete_material_plan_dao import DiscreteMaterialPlanDAO
|
||||||
|
self.dao = DiscreteMaterialPlanDAO()
|
||||||
|
except ImportError:
|
||||||
|
self._log("无法加载数据库 DAO 模块,持久化功能将不可用", "error")
|
||||||
|
|
||||||
|
def _log(self, message, level="info"):
|
||||||
|
"""
|
||||||
|
统一日志出口:同步分发到控制台和 UI 回调
|
||||||
|
"""
|
||||||
|
level = level.lower()
|
||||||
|
# 1. 记录到标准控制台
|
||||||
|
log_map = {
|
||||||
|
"info": logger.info,
|
||||||
|
"warn": logger.warning,
|
||||||
|
"error": logger.error
|
||||||
|
}
|
||||||
|
log_func = log_map.get(level, logger.info)
|
||||||
|
log_func(message)
|
||||||
|
|
||||||
|
# 2. 同步到 UI
|
||||||
|
# 优化:发送原始 message,让 UI 控件自行添加时间戳,确保格式统一且不报错
|
||||||
|
if self.progress_callback:
|
||||||
|
self._report_progress("log", 0, 0, message, log_level=level.upper())
|
||||||
|
|
||||||
|
def _report_progress(self, stage: str, current: int, total: int, message: str, **detail):
|
||||||
|
"""标准化进度汇报"""
|
||||||
|
if self.progress_callback and ProgressInfo:
|
||||||
|
try:
|
||||||
progress_info = ProgressInfo(
|
progress_info = ProgressInfo(
|
||||||
stage=stage,
|
stage=stage,
|
||||||
current=current,
|
current=current,
|
||||||
@@ -78,522 +93,190 @@ class DiscreteMaterialPlanExtractor:
|
|||||||
)
|
)
|
||||||
self.progress_callback(progress_info)
|
self.progress_callback(progress_info)
|
||||||
except Exception:
|
except Exception:
|
||||||
# 如果进度回调失败,忽略错误,不影响主流程
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_production_order_numbers(self, production_id_file, report_progress=False):
|
def get_production_order_numbers(self, production_id_file, report_progress=False):
|
||||||
"""
|
"""读取总排号并查询数据库获取生产订单号"""
|
||||||
读取总排号文件并查询数据库获取生产订单号
|
|
||||||
|
|
||||||
Args:
|
|
||||||
production_id_file: ProductionID.txt 文件路径
|
|
||||||
report_progress: 是否报告进度
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
生产订单号列表
|
|
||||||
"""
|
|
||||||
if report_progress:
|
if report_progress:
|
||||||
self._report_progress(
|
self._report_progress("query", 1, 3, "正在读取总排号文件...", action="read_file")
|
||||||
"query",
|
|
||||||
1,
|
|
||||||
3,
|
|
||||||
"正在读取总排号文件...",
|
|
||||||
action="read_file",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 读取总排号
|
|
||||||
production_ids = read_production_ids(production_id_file)
|
production_ids = read_production_ids(production_id_file)
|
||||||
self._print(f"从文件读取到 {len(production_ids)} 个总排号")
|
self._log(f"文件读取完成: 找到 {len(production_ids)} 个 Production ID")
|
||||||
|
|
||||||
if report_progress:
|
if report_progress:
|
||||||
self._report_progress(
|
self._report_progress("query", 2, 3, "正在查询数据库获取生产订单号...", action="query_database")
|
||||||
"query",
|
|
||||||
2,
|
|
||||||
3,
|
|
||||||
f"正在查询数据库({len(production_ids)} 个总排号)...",
|
|
||||||
action="query_database",
|
|
||||||
production_id_count=len(production_ids),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 查询数据库获取生产订单号
|
|
||||||
order_ids = query_production_order_numbers(production_ids)
|
order_ids = query_production_order_numbers(production_ids)
|
||||||
self._print(f"查询到 {len(order_ids)} 个生产订单号")
|
self._log(f"数据库查询完成: 共匹配到 {len(order_ids)} 条生产订单号")
|
||||||
|
|
||||||
if report_progress:
|
if report_progress:
|
||||||
self._report_progress(
|
self._report_progress("query", 3, 3, "订单号查询阶段结束", action="query_complete")
|
||||||
"query",
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
f"查询完成:获取到 {len(order_ids)} 个生产订单号",
|
|
||||||
action="query_complete",
|
|
||||||
order_id_count=len(order_ids),
|
|
||||||
)
|
|
||||||
|
|
||||||
return order_ids
|
return order_ids
|
||||||
|
|
||||||
def group_order_ids(self, order_ids, group_size=100):
|
def group_order_ids(self, order_ids, group_size=100):
|
||||||
"""将订单号分组"""
|
"""生成器:按批次切割订单号"""
|
||||||
for i in range(0, len(order_ids), group_size):
|
for i in range(0, len(order_ids), group_size):
|
||||||
yield order_ids[i : i + group_size]
|
yield order_ids[i : i + group_size]
|
||||||
|
|
||||||
def download_batch(
|
def download_batch(self, inner_frame, order_ids, batch_index, total_batches, page1):
|
||||||
self,
|
"""执行单批次数据的下载流程"""
|
||||||
inner_frame,
|
self._report_progress("download", batch_index * 7 + 1, total_batches * 7,
|
||||||
order_ids,
|
f"第 {batch_index + 1} 批: 正在填充订单号", action="fill_orders")
|
||||||
batch_index,
|
|
||||||
total_batches,
|
|
||||||
page1,
|
|
||||||
debug_mode=False,
|
|
||||||
debug_batch=None,
|
|
||||||
):
|
|
||||||
"""下载一批订单号的数据"""
|
|
||||||
from playwright.sync_api import TimeoutError
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
# 步骤1:清空文本框
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
batch_index * 7 + 1,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批 - 准备输入订单号",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="clear_textbox",
|
|
||||||
)
|
|
||||||
textbox = inner_frame.get_by_role("textbox", name="来源生产订单号")
|
textbox = inner_frame.get_by_role("textbox", name="来源生产订单号")
|
||||||
textbox.fill("")
|
textbox.fill("")
|
||||||
|
|
||||||
# 步骤2:填充订单号
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
batch_index * 7 + 2,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批 - 输入 {len(order_ids)} 个订单号",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="fill_order_ids",
|
|
||||||
order_count=len(order_ids),
|
|
||||||
)
|
|
||||||
textbox.fill(",".join(order_ids))
|
textbox.fill(",".join(order_ids))
|
||||||
|
|
||||||
# 步骤3:点击查询
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
batch_index * 7 + 3,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批 - 提交查询请求",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="click_search",
|
|
||||||
)
|
|
||||||
inner_frame.locator(".search-component-searchBtn").click()
|
inner_frame.locator(".search-component-searchBtn").click()
|
||||||
self._print(f"第 {batch_index + 1} 批查询完成,等待加载结果...")
|
|
||||||
|
|
||||||
# 步骤4:等待加载完成
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
batch_index * 7 + 4,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批 - 等待数据加载...",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="wait_loading",
|
|
||||||
)
|
|
||||||
loading_locator = inner_frame.locator("div").filter(has_text="加载中").nth(1)
|
loading_locator = inner_frame.locator("div").filter(has_text="加载中").nth(1)
|
||||||
try:
|
try:
|
||||||
loading_locator.wait_for(state="visible", timeout=3000)
|
loading_locator.wait_for(state="visible", timeout=3000)
|
||||||
loading_locator.wait_for(state="hidden", timeout=0) # 无限等待,直到消失
|
loading_locator.wait_for(state="hidden", timeout=0)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
# 加载很快完成,或者没有出现加载提示
|
|
||||||
pass
|
pass
|
||||||
self._print(f"第 {batch_index + 1} 批加载完成,开始选择数据...")
|
|
||||||
|
|
||||||
# 调试模式:只在指定批次暂停
|
|
||||||
if debug_mode and (debug_batch is None or batch_index == debug_batch):
|
|
||||||
self._print(f"=== 调试暂停:第 {batch_index + 1} 批 ===")
|
|
||||||
page1.pause()
|
|
||||||
|
|
||||||
# 步骤5:选择所有数据
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
batch_index * 7 + 5,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批 - 选择所有数据行",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="select_all_rows",
|
|
||||||
)
|
|
||||||
inner_frame.get_by_role("row", name="序号").get_by_label("").click()
|
inner_frame.get_by_role("row", name="序号").get_by_label("").click()
|
||||||
|
|
||||||
# 步骤6:配置并触发导出
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
batch_index * 7 + 6,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批 - 配置导出参数",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="configure_export",
|
|
||||||
)
|
|
||||||
|
|
||||||
# 点击输出
|
|
||||||
inner_frame.get_by_role("button", name="更多").hover()
|
inner_frame.get_by_role("button", name="更多").hover()
|
||||||
inner_frame.get_by_text("输出", exact=True).click()
|
inner_frame.get_by_text("输出", exact=True).click()
|
||||||
|
|
||||||
|
threshold_box = inner_frame.locator("div").filter(has_text=re.compile(r"^行数阈值$")).locator("input[type='text']")
|
||||||
|
threshold_box.fill("300000")
|
||||||
|
|
||||||
# 设置行数阈值
|
|
||||||
input_box = (
|
|
||||||
inner_frame.locator("div")
|
|
||||||
.filter(has_text=re.compile(r"^行数阈值$"))
|
|
||||||
.locator("input[type='text']")
|
|
||||||
)
|
|
||||||
input_box.fill("300000")
|
|
||||||
|
|
||||||
# 步骤7:下载文件
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
batch_index * 7 + 7,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批 - 正在下载文件...",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="downloading_file",
|
|
||||||
)
|
|
||||||
download_path = f"D:/python/playwrite/data/temp_batch_{batch_index + 1}.xlsx"
|
download_path = f"D:/python/playwrite/data/temp_batch_{batch_index + 1}.xlsx"
|
||||||
with page1.expect_download() as download_info:
|
with page1.expect_download() as download_info:
|
||||||
inner_frame.get_by_role("button", name="确定(Y)").click()
|
inner_frame.get_by_role("button", name="确定(Y)").click()
|
||||||
|
|
||||||
download = download_info.value
|
download = download_info.value
|
||||||
download.save_as(download_path)
|
download.save_as(download_path)
|
||||||
self._print(f"第 {batch_index + 1} 批下载完成: {download_path}")
|
self._log(f"批次 {batch_index + 1} 下载成功 -> {download_path}")
|
||||||
|
|
||||||
# 报告批次完成
|
|
||||||
self._report_progress(
|
|
||||||
"download",
|
|
||||||
(batch_index + 1) * 7,
|
|
||||||
total_batches * 7,
|
|
||||||
f"第 {batch_index + 1}/{total_batches} 批下载完成 ✓",
|
|
||||||
batch_index=batch_index + 1,
|
|
||||||
action="batch_complete",
|
|
||||||
file_path=download_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 等待页面恢复,准备下一次查询
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
return download_path
|
return download_path
|
||||||
|
|
||||||
def convert_and_merge_files(self, file_paths, output_path):
|
def convert_and_merge_files(self, file_paths, output_path):
|
||||||
"""使用 ExcelConverter 转换并合并所有文件,返回合并后的 DataFrame"""
|
"""合并 Excel 文件并清理临时文件"""
|
||||||
# 确保输出文件路径是正确的格式
|
|
||||||
output_path = os.path.normpath(output_path)
|
output_path = os.path.normpath(output_path)
|
||||||
output_dir = os.path.dirname(output_path)
|
output_dir = os.path.dirname(output_path)
|
||||||
output_filename = os.path.basename(output_path)
|
|
||||||
|
|
||||||
self._print(f"输出文件路径: {output_path}")
|
|
||||||
self._print(f"输出目录: {output_dir}")
|
|
||||||
self._print(f"输出文件名: {output_filename}")
|
|
||||||
|
|
||||||
# 步骤1:检查并创建输出目录
|
|
||||||
self._report_progress(
|
|
||||||
"convert",
|
|
||||||
1,
|
|
||||||
len(file_paths) * 2 + 3,
|
|
||||||
"准备转换:检查输出目录",
|
|
||||||
action="check_directory",
|
|
||||||
)
|
|
||||||
|
|
||||||
if output_dir and not os.path.exists(output_dir):
|
if output_dir and not os.path.exists(output_dir):
|
||||||
self._print(f"创建输出目录: {output_dir}")
|
|
||||||
os.makedirs(output_dir)
|
os.makedirs(output_dir)
|
||||||
|
|
||||||
all_dataframes = []
|
all_dfs = []
|
||||||
|
total_steps = len(file_paths) * 2 + 3
|
||||||
|
|
||||||
# 步骤2-N:转换每个文件
|
for i, path in enumerate(file_paths, 1):
|
||||||
for i, file_path in enumerate(file_paths, 1):
|
self._report_progress("convert", 1 + (i-1)*2 + 1, total_steps, f"正在转换 Excel {i}/{len(file_paths)}")
|
||||||
self._print(f"转换第 {i} 个文件: {file_path}")
|
df = self.converter.convert(path, output_file=None)
|
||||||
|
all_dfs.append(df)
|
||||||
|
self._log(f"文件 {i} 转换完成: 提取到 {len(df)} 条记录")
|
||||||
|
|
||||||
# 报告开始转换
|
if all_dfs:
|
||||||
self._report_progress(
|
self._report_progress("convert", total_steps - 1, total_steps, "正在进行最终数据合并...")
|
||||||
"convert",
|
merged_df = pd.concat(all_dfs, ignore_index=True)
|
||||||
1 + (i - 1) * 2 + 1,
|
|
||||||
len(file_paths) * 2 + 3,
|
|
||||||
f"正在转换文件 {i}/{len(file_paths)}",
|
|
||||||
file_index=i,
|
|
||||||
file_path=file_path,
|
|
||||||
action="converting_file",
|
|
||||||
)
|
|
||||||
|
|
||||||
df = self.converter.convert(file_path, output_file=None) # 只转换,不保存
|
|
||||||
all_dataframes.append(df)
|
|
||||||
self._print(f" 提取到 {len(df)} 条记录")
|
|
||||||
|
|
||||||
# 报告转换完成
|
|
||||||
self._report_progress(
|
|
||||||
"convert",
|
|
||||||
1 + (i - 1) * 2 + 2,
|
|
||||||
len(file_paths) * 2 + 3,
|
|
||||||
f"文件 {i}/{len(file_paths)} 转换完成({len(df)} 条记录)",
|
|
||||||
file_index=i,
|
|
||||||
record_count=len(df),
|
|
||||||
action="file_converted",
|
|
||||||
)
|
|
||||||
|
|
||||||
merged_df = None
|
|
||||||
if all_dataframes:
|
|
||||||
# 步骤N+1:合并数据
|
|
||||||
self._report_progress(
|
|
||||||
"convert",
|
|
||||||
len(file_paths) * 2 + 2,
|
|
||||||
len(file_paths) * 2 + 3,
|
|
||||||
f"正在合并 {len(all_dataframes)} 个文件的数据...",
|
|
||||||
action="merging_data",
|
|
||||||
file_count=len(all_dataframes),
|
|
||||||
)
|
|
||||||
|
|
||||||
self._print(f"\n合并 {len(all_dataframes)} 个文件的数据...")
|
|
||||||
merged_df = pd.concat(all_dataframes, ignore_index=True)
|
|
||||||
merged_df.to_excel(output_path, index=False)
|
merged_df.to_excel(output_path, index=False)
|
||||||
self._print(f"合并完成: {output_path}, 总共 {len(merged_df)} 条记录")
|
|
||||||
|
for p in file_paths:
|
||||||
# 步骤N+2:删除临时文件
|
try: os.remove(p)
|
||||||
self._report_progress(
|
except: pass
|
||||||
"convert",
|
|
||||||
len(file_paths) * 2 + 3,
|
|
||||||
len(file_paths) * 2 + 3,
|
|
||||||
f"清理临时文件...",
|
|
||||||
action="cleanup",
|
|
||||||
total_records=len(merged_df),
|
|
||||||
)
|
|
||||||
|
|
||||||
for file_path in file_paths:
|
|
||||||
os.remove(file_path)
|
|
||||||
self._print(f"已删除临时文件: {file_path}")
|
|
||||||
|
|
||||||
return output_path, merged_df
|
return output_path, merged_df
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def _save_to_database(self, df: pd.DataFrame):
|
def _save_to_database(self, df: pd.DataFrame):
|
||||||
"""Save DataFrame to database with progress reporting"""
|
"""将结果存入数据库并打印详细统计信息"""
|
||||||
|
if not self.dao: return
|
||||||
try:
|
try:
|
||||||
self._report_progress(
|
self._report_progress("database", 1, 3, "正在将数据同步至数据库...")
|
||||||
"database", 0, 3, "准备保存到数据库...",
|
# 使用 with 关键字确保资源安全释放
|
||||||
action="db_start"
|
with self.dao as db:
|
||||||
)
|
stats = db.save_dataframe_with_replace(df)
|
||||||
|
|
||||||
stats = self.dao.save_dataframe_with_replace(df)
|
# 保留并输出完整的处理细节:删除条数和新增条数
|
||||||
|
msg = f"数据库保存完成: 删除 {stats.get('deleted', 0)} 条, 新增 {stats.get('inserted', 0)} 条"
|
||||||
self._report_progress(
|
self._log(msg, "info")
|
||||||
"database", 3, 3,
|
|
||||||
f"数据库保存完成: 删除 {stats['deleted']} 条, 新增 {stats['inserted']} 条",
|
|
||||||
action="db_complete",
|
|
||||||
stats=stats
|
|
||||||
)
|
|
||||||
|
|
||||||
self._print(f"\n数据库保存成功:")
|
|
||||||
self._print(f" 删除旧记录: {stats['deleted']} 条")
|
|
||||||
self._print(f" 新增记录: {stats['inserted']} 条")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._print(f"\n警告: 数据库保存失败: {e}")
|
self._log(f"数据库保存失败: {str(e)}", "error")
|
||||||
self._report_progress(
|
|
||||||
"database", 3, 3,
|
|
||||||
f"数据库保存失败: {str(e)}",
|
|
||||||
action="db_error",
|
|
||||||
error=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
def setup_query_interface(self, inner_frame):
|
def setup_query_interface(self, inner_frame):
|
||||||
"""设置查询界面(不报告进度,由 extract 统一报告)"""
|
"""初始化查询界面"""
|
||||||
import re
|
|
||||||
|
|
||||||
# 打开查询界面
|
|
||||||
inner_frame.locator(".search-name-wrapper > .iconfont").click()
|
inner_frame.locator(".search-name-wrapper > .iconfont").click()
|
||||||
inner_frame.get_by_text("订单号查询").click()
|
inner_frame.get_by_text("订单号查询").click()
|
||||||
|
|
||||||
# 选择"全部"标签
|
|
||||||
inner_frame.get_by_role("tab", name="全部").click()
|
inner_frame.get_by_role("tab", name="全部").click()
|
||||||
|
|
||||||
# 填充并验证
|
input_box = inner_frame.locator("#rc_select_0")
|
||||||
max_retries = 3
|
input_box.fill("5000")
|
||||||
expected_value = "5000"
|
input_box.press("Enter")
|
||||||
for attempt in range(max_retries):
|
|
||||||
inner_frame.locator("#rc_select_0").fill(expected_value)
|
|
||||||
inner_frame.locator("#rc_select_0").press("Enter")
|
|
||||||
|
|
||||||
actual_value = inner_frame.locator("#rc_select_0").input_value()
|
|
||||||
if actual_value == expected_value:
|
|
||||||
self._print(f"文本框填充成功: {expected_value}")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self._print(
|
|
||||||
f"第 {attempt + 1} 次填充失败,实际值: {actual_value},重试..."
|
|
||||||
)
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
self._print(
|
|
||||||
f"警告: {max_retries} 次尝试后仍未成功填充,继续执行..."
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract(
|
def extract(
|
||||||
self,
|
self, production_id_file, output_file="D:/python/playwrite/data/离散备料计划维护_合并.xlsx",
|
||||||
production_id_file,
|
progress_callback=None
|
||||||
data_dir="D:/python/playwrite/data",
|
|
||||||
output_file="D:/python/playwrite/data/离散备料计划维护_合并.xlsx",
|
|
||||||
debug_mode=False,
|
|
||||||
debug_batch=None,
|
|
||||||
progress_callback=None,
|
|
||||||
):
|
):
|
||||||
"""执行完整的数据提取流程"""
|
"""主入口:执行全流程数据提取任务"""
|
||||||
original_callback = self.progress_callback
|
self.progress_callback = progress_callback
|
||||||
self.progress_callback = progress_callback or self.progress_callback
|
downloaded_files = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with sync_playwright() as playwright:
|
with sync_playwright() as playwright:
|
||||||
# 步骤1:启动浏览器并登录
|
self._report_progress("login", 1, 3, "启动浏览器并尝试登录 ERP...")
|
||||||
self._report_progress(
|
|
||||||
"login",
|
|
||||||
1,
|
|
||||||
3, # 保持 3 步
|
|
||||||
"启动浏览器并登录...",
|
|
||||||
action="launch_browser",
|
|
||||||
)
|
|
||||||
|
|
||||||
browser, context, page, main_frame = login(
|
browser, context, page, main_frame = login(
|
||||||
playwright=playwright,
|
playwright=playwright, username=self.username, password=self.password,
|
||||||
username=self.username,
|
headless=self.headless, ignore_https_errors=True
|
||||||
password=self.password,
|
|
||||||
headless=self.headless,
|
|
||||||
ignore_https_errors=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 步骤2:打开功能页面
|
self._log("======================================== 开始执行数据提取任务 ========================================")
|
||||||
self._report_progress(
|
|
||||||
"login",
|
|
||||||
2,
|
|
||||||
3, # 保持 3 步
|
|
||||||
"登录成功,打开功能页面...",
|
|
||||||
action="open_function_page",
|
|
||||||
)
|
|
||||||
|
|
||||||
self._print("=" * 80)
|
|
||||||
self._print("开始执行离散备料计划维护数据提取")
|
|
||||||
self._print("=" * 80)
|
|
||||||
|
|
||||||
main_frame.locator("i").first.click()
|
main_frame.locator("i").first.click()
|
||||||
|
|
||||||
with page.expect_popup() as page1_info:
|
with page.expect_popup() as page1_info:
|
||||||
main_frame.get_by_title(
|
main_frame.get_by_title("离散备料计划维护", exact=True).first.click()
|
||||||
"离散备料计划维护", exact=True
|
|
||||||
).first.click()
|
|
||||||
page1 = page1_info.value
|
page1 = page1_info.value
|
||||||
|
|
||||||
main_frame = page1.locator("#forwardFrame").content_frame
|
f_frame = page1.locator("#forwardFrame").content_frame
|
||||||
inner_frame_locator = main_frame.locator("#mainiframe")
|
inner_frame_locator = f_frame.locator("#mainiframe")
|
||||||
inner_frame_locator.wait_for(state="visible", timeout=15000)
|
inner_frame_locator.wait_for(state="visible", timeout=15000)
|
||||||
inner_frame = inner_frame_locator.content_frame
|
work_frame = inner_frame_locator.content_frame
|
||||||
|
|
||||||
# 步骤3:设置查询界面
|
self.setup_query_interface(work_frame)
|
||||||
self._report_progress(
|
order_ids = self.get_production_order_numbers(production_id_file, report_progress=True)
|
||||||
"login",
|
|
||||||
3,
|
|
||||||
3, # 保持 3 步
|
|
||||||
"配置查询界面...",
|
|
||||||
action="setup_query_interface",
|
|
||||||
)
|
|
||||||
self.setup_query_interface(inner_frame)
|
|
||||||
|
|
||||||
# 后续代码保持不变...
|
batch_list = list(self.group_order_ids(order_ids, self.batch_size))
|
||||||
order_ids = self.get_production_order_numbers(
|
for i, batch_ids in enumerate(batch_list):
|
||||||
production_id_file, report_progress=True
|
self._log(f"正在处理第 {i+1} 批次 (共 {len(batch_list)} 批)")
|
||||||
)
|
try:
|
||||||
|
f_path = self.download_batch(work_frame, batch_ids, i, len(batch_list), page1)
|
||||||
downloaded_files = []
|
downloaded_files.append(f_path)
|
||||||
total_batches = sum(
|
except Exception as e:
|
||||||
1 for _ in self.group_order_ids(order_ids, self.batch_size)
|
self._log(f"批次 {i+1} 处理异常,已跳过。详细错误: {e}", "error")
|
||||||
)
|
continue
|
||||||
|
|
||||||
for batch_index, order_ids_batch in enumerate(
|
|
||||||
self.group_order_ids(order_ids, self.batch_size)
|
|
||||||
):
|
|
||||||
self._print(
|
|
||||||
f"\n=== 开始处理第 {batch_index + 1} 批,共 {len(order_ids_batch)} 个订单号 ==="
|
|
||||||
)
|
|
||||||
|
|
||||||
downloaded_file = self.download_batch(
|
|
||||||
inner_frame,
|
|
||||||
order_ids_batch,
|
|
||||||
batch_index,
|
|
||||||
total_batches,
|
|
||||||
page1,
|
|
||||||
debug_mode=debug_mode,
|
|
||||||
debug_batch=debug_batch,
|
|
||||||
)
|
|
||||||
downloaded_files.append(downloaded_file)
|
|
||||||
|
|
||||||
self._print("\n开始执行账号注销...")
|
|
||||||
self._report_progress(
|
|
||||||
"logout",
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
"正在注销账号...",
|
|
||||||
action="logout_start",
|
|
||||||
)
|
|
||||||
logout(main_frame, verbose=self.verbose)
|
|
||||||
|
|
||||||
self._report_progress(
|
|
||||||
"logout",
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
"注销完成 ✓",
|
|
||||||
action="logout_complete",
|
|
||||||
)
|
|
||||||
|
|
||||||
if downloaded_files:
|
|
||||||
self._print(
|
|
||||||
f"\n=== 开始转换并合并 {len(downloaded_files)} 个文件 ==="
|
|
||||||
)
|
|
||||||
output_path, merged_df = self.convert_and_merge_files(downloaded_files, output_file)
|
|
||||||
|
|
||||||
# 数据库保存步骤(独立阶段)
|
|
||||||
if self.enable_db_persistence and self.dao and merged_df is not None:
|
|
||||||
self._print(f"\n=== 开始保存数据到数据库 ===")
|
|
||||||
self._save_to_database(merged_df)
|
|
||||||
else:
|
|
||||||
self._print("\n没有下载到任何文件")
|
|
||||||
|
|
||||||
self._print(f"\n=== 全部完成 ===")
|
|
||||||
self._print(f"最终文件: {output_file}")
|
|
||||||
|
|
||||||
self._report_progress(
|
|
||||||
"complete", 1, 1, "数据提取完成 ✓",
|
|
||||||
output_file=output_file,
|
|
||||||
action="all_complete",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
self._log("正在注销并关闭浏览器环境...")
|
||||||
|
logout(f_frame, verbose=self.verbose)
|
||||||
context.close()
|
context.close()
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
return output_file
|
if downloaded_files:
|
||||||
|
final_path, final_df = self.convert_and_merge_files(downloaded_files, output_file)
|
||||||
|
if self.enable_db_persistence and final_df is not None:
|
||||||
|
self._save_to_database(final_df)
|
||||||
|
|
||||||
|
self._log(f"所有流程已顺利结束,结果文件: {final_path}")
|
||||||
|
self._report_progress("complete", 1, 1, "任务完成")
|
||||||
|
return final_path
|
||||||
|
|
||||||
|
self._log("未获得任何有效数据,任务终止", "warn")
|
||||||
|
return None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Close database connection if open
|
self.progress_callback = None
|
||||||
if self.dao:
|
|
||||||
try:
|
|
||||||
self.dao.__exit__(None, None, None)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
self.progress_callback = original_callback
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""测试函数"""
|
|
||||||
extractor = DiscreteMaterialPlanExtractor(
|
extractor = DiscreteMaterialPlanExtractor(
|
||||||
username="BLDpengqiangqiang",
|
username="BLDpengqiangqiang",
|
||||||
password="Cqbld123456.",
|
password="your_password",
|
||||||
headless=False,
|
enable_db_persistence=True
|
||||||
verbose=True,
|
|
||||||
)
|
)
|
||||||
|
id_file = os.path.join(os.path.dirname(__file__), "productionID.txt")
|
||||||
production_id_file = os.path.join(os.path.dirname(__file__), "productionID.txt")
|
extractor.extract(id_file)
|
||||||
output_file = "D:/python/playwrite/data/离散备料计划维护_合并.xlsx"
|
|
||||||
|
|
||||||
extractor.extract(production_id_file, output_file)
|
|
||||||
|
|
||||||
input("按回车退出...")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -1,10 +1,19 @@
|
|||||||
"""
|
"""
|
||||||
离散备料计划维护数据清理工具
|
离散备料计划维护数据清理工具
|
||||||
负责登录、逐个清理订单数据
|
功能:自动登录 ERP 系统,根据负责人姓名批量清理指定的备料计划物料。
|
||||||
|
优化点:
|
||||||
|
1. 数据库预取:从 $O(n)$ 次数据库查询优化为 $O(1)$ 内存匹配(HashSet)。
|
||||||
|
2. 日志规范:使用 logging 模块替代 print。
|
||||||
|
3. 代码整洁:移除方法内导入,增加通用定位辅助函数。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from playwright.sync_api import sync_playwright
|
import re
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from playwright.sync_api import sync_playwright, TimeoutError
|
||||||
|
|
||||||
|
# 统一顶部导入
|
||||||
from utils.auth import login, logout
|
from utils.auth import login, logout
|
||||||
from db.production_order_query import (
|
from db.production_order_query import (
|
||||||
read_production_ids,
|
read_production_ids,
|
||||||
@@ -12,299 +21,167 @@ from db.production_order_query import (
|
|||||||
)
|
)
|
||||||
from db.materials_to_delete import get_materials_to_delete
|
from db.materials_to_delete import get_materials_to_delete
|
||||||
|
|
||||||
|
# --- 日志配置 ---
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||||
|
datefmt='%Y-%m-%d %H:%M:%S'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class DiscreteMaterialPlanCleaner:
|
class DiscreteMaterialPlanCleaner:
|
||||||
"""离散备料计划维护数据清理器"""
|
"""离散备料计划维护数据清理器"""
|
||||||
|
|
||||||
def __init__(self, username, password, manager_name, headless=False, verbose=True):
|
def __init__(self, username, password, manager_name, headless=False, verbose=True):
|
||||||
"""
|
|
||||||
初始化清理器
|
|
||||||
|
|
||||||
Args:
|
|
||||||
username: 登录用户名
|
|
||||||
password: 登录密码
|
|
||||||
manager_name: 负责人姓名
|
|
||||||
headless: 是否无头模式运行
|
|
||||||
verbose: 是否打印详细日志
|
|
||||||
"""
|
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.manager_name = manager_name
|
self.manager_name = manager_name
|
||||||
self.headless = headless
|
self.headless = headless
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
|
# 核心优化:使用 set 存储待删除编码,查询复杂度为 $O(1)$
|
||||||
|
self.to_delete_set = set()
|
||||||
|
|
||||||
def _print(self, *args, **kwargs):
|
def _log(self, message, level="info"):
|
||||||
"""打印日志(如果 verbose=True)"""
|
"""统一日志输出控制"""
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print(*args, **kwargs)
|
if level == "info": logger.info(message)
|
||||||
|
elif level == "warn": logger.warning(message)
|
||||||
|
elif level == "error": logger.error(message)
|
||||||
|
|
||||||
|
def _is_button_enabled(self, button_locator):
|
||||||
|
"""判定按钮是否可用"""
|
||||||
|
try:
|
||||||
|
return button_locator.is_enabled()
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"检查按钮状态时出错: {e}", "error")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_input_value(self, container, label_regex):
|
||||||
|
"""通用辅助函数:根据 Label 正则获取 Input 的值"""
|
||||||
|
return container.locator("div").filter(
|
||||||
|
has_text=re.compile(label_regex, re.MULTILINE)
|
||||||
|
).locator("input").first.input_value()
|
||||||
|
|
||||||
|
def preload_data(self):
|
||||||
|
"""批量预取数据库数据"""
|
||||||
|
self._log(f"正在从数据库提取负责人 [{self.manager_name}] 的待删除物料清单...")
|
||||||
|
# 假设返回的是 material_code 列表
|
||||||
|
raw_list = get_materials_to_delete(self.manager_name)
|
||||||
|
self.to_delete_set = set(raw_list)
|
||||||
|
self._log(f"预加载完成,共计 {len(self.to_delete_set)} 条不合规物料编码。")
|
||||||
|
|
||||||
def get_production_order_numbers(self, production_id_file):
|
def get_production_order_numbers(self, production_id_file):
|
||||||
"""
|
"""读取文件并查询生产订单号"""
|
||||||
读取总排号文件并查询数据库获取生产订单号
|
|
||||||
|
|
||||||
Args:
|
|
||||||
production_id_file: ProductionID.txt 文件路径
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
生产订单号列表
|
|
||||||
"""
|
|
||||||
# 读取总排号
|
|
||||||
production_ids = read_production_ids(production_id_file)
|
production_ids = read_production_ids(production_id_file)
|
||||||
self._print(f"从文件读取到 {len(production_ids)} 个总排号")
|
|
||||||
|
|
||||||
# 查询数据库获取生产订单号
|
|
||||||
order_ids = query_production_order_numbers(production_ids)
|
order_ids = query_production_order_numbers(production_ids)
|
||||||
self._print(f"查询到 {len(order_ids)} 个生产订单号")
|
self._log(f"读取到 {len(production_ids)} 个总排号 -> 匹配到 {len(order_ids)} 个生产订单号")
|
||||||
|
|
||||||
return order_ids
|
return order_ids
|
||||||
|
|
||||||
def process_order(
|
def process_order(self, inner_frame, order_id, order_index, page1):
|
||||||
self,
|
"""清理单个订单的数据"""
|
||||||
inner_frame,
|
# 1. 查询订单
|
||||||
order_id,
|
|
||||||
order_index,
|
|
||||||
page1,
|
|
||||||
manager_name=None,
|
|
||||||
debug_mode=False,
|
|
||||||
debug_order=None,
|
|
||||||
):
|
|
||||||
"""清理单个订单的数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
inner_frame: 内部 iframe
|
|
||||||
order_id: 订单号
|
|
||||||
order_index: 订单索引
|
|
||||||
page1: 页面对象
|
|
||||||
manager_name: 负责人姓名(用于查询待删除物料)
|
|
||||||
debug_mode: 是否启用调试模式
|
|
||||||
debug_order: 调试订单号
|
|
||||||
"""
|
|
||||||
if manager_name is None:
|
|
||||||
manager_name = self.manager_name
|
|
||||||
from playwright.sync_api import TimeoutError
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
|
|
||||||
# 清空文本框
|
|
||||||
textbox = inner_frame.get_by_role("textbox", name="生产订单号")
|
textbox = inner_frame.get_by_role("textbox", name="生产订单号")
|
||||||
textbox.fill("")
|
|
||||||
|
|
||||||
# 填充订单号
|
|
||||||
textbox.fill(order_id)
|
textbox.fill(order_id)
|
||||||
|
|
||||||
# 点击查询
|
|
||||||
inner_frame.locator(".search-component-searchBtn").click()
|
inner_frame.locator(".search-component-searchBtn").click()
|
||||||
self._print(f"第 {order_index + 1} 个订单查询完成,等待加载结果...")
|
|
||||||
|
|
||||||
# 等待加载完成
|
# 2. 等待加载(改进:增加 60s 安全超时,防止死锁)
|
||||||
loading_locator = inner_frame.locator("div").filter(has_text="加载中").nth(1)
|
loading_locator = inner_frame.locator("div").filter(has_text="加载中").nth(1)
|
||||||
try:
|
try:
|
||||||
loading_locator.wait_for(state="visible", timeout=3000)
|
loading_locator.wait_for(state="visible", timeout=3000)
|
||||||
loading_locator.wait_for(state="hidden", timeout=0) # 无限等待,直到消失
|
loading_locator.wait_for(state="hidden", timeout=60000)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
# 加载很快完成,或者没有出现加载提示
|
|
||||||
pass
|
pass
|
||||||
self._print(f"第 {order_index + 1} 个订单加载完成,开始清理数据...")
|
|
||||||
|
|
||||||
# 调试模式:只在指定订单暂停
|
|
||||||
if debug_mode and (debug_order is None or order_index == debug_order):
|
|
||||||
self._print(f"=== 调试暂停:第 {order_index + 1} 个订单 ===")
|
|
||||||
page1.pause()
|
|
||||||
|
|
||||||
|
# 3. 进入备料计划详情
|
||||||
inner_frame.locator("#hot-key-head_list").get_by_text("更多").click()
|
inner_frame.locator("#hot-key-head_list").get_by_text("更多").click()
|
||||||
|
|
||||||
with page1.expect_popup() as page2_info:
|
with page1.expect_popup() as page2_info:
|
||||||
inner_frame.get_by_text("备料计划").click()
|
inner_frame.get_by_text("备料计划").click()
|
||||||
page2 = page2_info.value
|
page2 = page2_info.value
|
||||||
|
|
||||||
# 获取 nested iframe
|
# 4. 穿透嵌套 Iframe
|
||||||
main_frame = page2.locator("#forwardFrame").content_frame
|
detail_main_frame = page2.locator("#forwardFrame").content_frame
|
||||||
inner_frame_locator = main_frame.locator("#mainiframe")
|
detail_inner_frame = detail_main_frame.locator("#mainiframe").content_frame
|
||||||
inner_frame_locator.wait_for(state="visible", timeout=15000)
|
|
||||||
inner_frame = inner_frame_locator.content_frame
|
|
||||||
|
|
||||||
# 等待备料计划页面加载完成(等待序列号加载出来)
|
# 5. 提取订单状态信息
|
||||||
self._print("等待备料计划页面加载完成...")
|
plan_code_locator = detail_inner_frame.get_by_text(re.compile(r"^离散备料计划维护:"))
|
||||||
plan_code_locator = 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+\)$")).inner_text()
|
||||||
|
detail_count = int(re.search(r"\((\d+)\)", detail_count_text).group(1))
|
||||||
|
|
||||||
|
status_text = detail_inner_frame.get_by_text(re.compile(r"^备料状态:.+$")).inner_text()
|
||||||
|
detail_status = re.search(r"备料状态:(.+)$", status_text.replace("\n", "")).group(1)
|
||||||
|
|
||||||
# 循环检查编码是否已加载
|
# 6. 执行清理逻辑
|
||||||
max_wait = 30 # 最多等待30秒
|
|
||||||
wait_interval = 0.5 # 每0.5秒检查一次
|
|
||||||
waited = 0
|
|
||||||
plan_code = None
|
|
||||||
while waited < max_wait:
|
|
||||||
plan_text = plan_code_locator.inner_text()
|
|
||||||
match = re.search(r"离散备料计划维护:(.+)", plan_text)
|
|
||||||
if match and match.group(1).strip():
|
|
||||||
plan_code = match.group(1).strip()
|
|
||||||
break
|
|
||||||
time.sleep(wait_interval)
|
|
||||||
waited += wait_interval
|
|
||||||
|
|
||||||
if plan_code:
|
|
||||||
self._print(f"备料计划页面加载完成,编码: {plan_code}")
|
|
||||||
else:
|
|
||||||
self._print(f"警告: 备料计划页面加载超时")
|
|
||||||
|
|
||||||
# 提取"详细信息"中的数字
|
|
||||||
detail_element = inner_frame.get_by_text(re.compile(r"^详细信息 \(\d+\)$"))
|
|
||||||
detail_text = detail_element.inner_text()
|
|
||||||
# 使用正则表达式提取括号中的数字
|
|
||||||
match = re.search(r"详细信息 \((\d+)\)", detail_text)
|
|
||||||
if match:
|
|
||||||
detail_count = int(match.group(1))
|
|
||||||
self._print(f"详细信息数量: {detail_count}")
|
|
||||||
|
|
||||||
# 提取"备料状态"信息
|
|
||||||
detail_element = inner_frame.get_by_text(re.compile(r"^备料状态:.+$"))
|
|
||||||
detail_text = detail_element.inner_text().replace("\n", "")
|
|
||||||
# 使用正则表达式提取括号中的数字
|
|
||||||
match = re.search(r"^备料状态:(.+)$", detail_text)
|
|
||||||
if match:
|
|
||||||
detail_status = match.group(1)
|
|
||||||
self._print(f"备料状态: {detail_status}")
|
|
||||||
|
|
||||||
# page2.pause()
|
|
||||||
if detail_count > 0 and detail_status == "审批通过":
|
if detail_count > 0 and detail_status == "审批通过":
|
||||||
inner_frame.get_by_role("button", name="修改").click()
|
# --- 点击修改并等待状态切换 (保留原逻辑) ---
|
||||||
save_button_locator = inner_frame.get_by_role("button", name="保存")
|
detail_inner_frame.get_by_role("button", name="修改").click()
|
||||||
|
|
||||||
|
# 关键判断:等待保存按钮出现,确认进入编辑模式
|
||||||
|
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=10000)
|
||||||
|
self._log("已进入编辑模式(保存按钮已就绪)")
|
||||||
|
# ---------------------------------------
|
||||||
|
|
||||||
inner_frame.get_by_text("展开").first.click()
|
detail_inner_frame.get_by_text("展开").first.click()
|
||||||
|
|
||||||
|
child_form = detail_inner_frame.locator(".card-table-side-box")
|
||||||
|
button_wrapper = child_form.locator(".button-wrapper")
|
||||||
|
|
||||||
|
delete_row_btn = button_wrapper.get_by_role("button", name="删行")
|
||||||
|
next_btn = button_wrapper.locator(".icon-jiantouyou")
|
||||||
|
collapse_btn = button_wrapper.locator(".icon-celashouqi")
|
||||||
|
|
||||||
# 获取展开后的父容器,基于它定位子元素更加精确
|
last_row_number = None
|
||||||
# 父元素 class="card-table-side-box undefined"
|
while True:
|
||||||
child_form = inner_frame.locator(".card-table-side-box")
|
# 稳定性检查:等待行号更新
|
||||||
# 等待父容器变为可见
|
current_row = self._get_input_value(child_form, r"^行号$")
|
||||||
child_form.wait_for(state="visible", timeout=5000)
|
if current_row == last_row_number:
|
||||||
self._print(f"父容器 .card-table-side-box 已找到")
|
time.sleep(0.5)
|
||||||
|
|
||||||
# page2.pause()
|
material_code = self._get_input_value(child_form, r"^材料编码")
|
||||||
for id in range(detail_count):
|
material_name = self._get_input_value(child_form, r"^材料名称")
|
||||||
id_lable_locator = child_form.get_by_text("序号 " + str(id + 1))
|
pending_qty = self._get_input_value(child_form, r"^累计待发数量$")
|
||||||
id_lable_locator.wait_for(state="visible", timeout=10000)
|
|
||||||
self._print(f"处理 {id_lable_locator.inner_text()} ")
|
|
||||||
|
|
||||||
# 获取材料编码(用于精确匹配)
|
if material_code in self.to_delete_set:
|
||||||
input_box = (
|
self._log(f"发现匹配物料: {material_name} ({material_code})")
|
||||||
child_form.locator("div")
|
if not pending_qty or float(pending_qty) == 0:
|
||||||
.filter(has_text=re.compile(r"^材料编码\d{11}$", re.MULTILINE))
|
delete_row_btn.click()
|
||||||
.locator("input")
|
self._log(f"✅ 已点击删行")
|
||||||
.first
|
continue
|
||||||
)
|
|
||||||
material_code = input_box.input_value()
|
|
||||||
self._print(f"材料编码:{material_code}")
|
|
||||||
|
|
||||||
# 获取材料名称(仅用于日志)
|
|
||||||
input_box = (
|
|
||||||
child_form.locator("div")
|
|
||||||
.filter(has_text=re.compile(r"^材料名称$"))
|
|
||||||
.locator("input[type='text']")
|
|
||||||
)
|
|
||||||
material_name = input_box.input_value()
|
|
||||||
self._print(f"材料名称:{material_name}")
|
|
||||||
|
|
||||||
# 获取累计待发数量
|
|
||||||
# 创建一个变量来保存累计待发数量的值,以便后续使用
|
|
||||||
cumulative_pending_quantity = 0
|
|
||||||
input_box = (
|
|
||||||
child_form.locator("div")
|
|
||||||
.filter(has_text=re.compile(r"^累计待发数量$"))
|
|
||||||
.locator("input[type='text']")
|
|
||||||
)
|
|
||||||
cumulative_pending_quantity = input_box.input_value()
|
|
||||||
self._print(f"累计待发数量:{cumulative_pending_quantity}")
|
|
||||||
|
|
||||||
# 获取累计出库数量
|
|
||||||
input_box = (
|
|
||||||
child_form.locator("div")
|
|
||||||
.filter(has_text=re.compile(r"^累计出库数量$"))
|
|
||||||
.locator("input[type='text']")
|
|
||||||
)
|
|
||||||
self._print(f"累计出库数量:{input_box.input_value()}")
|
|
||||||
|
|
||||||
# 检查是否需要清理该物料(直接数据库查询)
|
|
||||||
from db.materials_to_delete import should_delete_material
|
|
||||||
should_delete = should_delete_material(manager_name, material_code)
|
|
||||||
|
|
||||||
if should_delete:
|
|
||||||
if not cumulative_pending_quantity :
|
|
||||||
self._print(f"✅ 可以删除")
|
|
||||||
else:
|
else:
|
||||||
self._print(f"❌ 累计待发数量不为空,无法删除")
|
self._log(f"⚠️ 待发数量为 {pending_qty},跳过删除", "warn")
|
||||||
self._print(
|
|
||||||
f">>> 需要清理:材料名称【{material_name}】材料编码【{material_code}】"
|
if self._is_button_enabled(next_btn):
|
||||||
)
|
last_row_number = current_row
|
||||||
# TODO: 执行删除操作
|
next_btn.click()
|
||||||
else:
|
else:
|
||||||
self._print(f"保留:材料名称【{material_name}】材料编码【{material_code}】无需清理")
|
break
|
||||||
|
|
||||||
if id != detail_count - 1:
|
collapse_btn.click()
|
||||||
child_form.get_by_role("button").filter(
|
|
||||||
has_text=re.compile(r"^$")
|
# 执行最终保存逻辑(如业务需要)
|
||||||
).nth(2).click()
|
# save_button_locator.click()
|
||||||
else:
|
|
||||||
child_form.get_by_role("button").filter(
|
|
||||||
has_text=re.compile(r"^$")
|
|
||||||
).nth(4).click()
|
|
||||||
# page2.pause()
|
|
||||||
|
|
||||||
elif detail_count == 0:
|
|
||||||
self._print(f"第 {order_index + 1} 个订单无数据需要清理,跳过...")
|
|
||||||
page2.close()
|
|
||||||
return
|
|
||||||
elif detail_status != "审批通过":
|
|
||||||
self._print(f"第 {order_index + 1} 个订单备料状态: {detail_status}")
|
|
||||||
page2.close()
|
|
||||||
return
|
|
||||||
|
|
||||||
page2.close()
|
page2.close()
|
||||||
time.sleep(1)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def setup_query_interface(self, inner_frame):
|
def setup_query_interface(self, inner_frame):
|
||||||
"""设置查询界面"""
|
"""初始化查询界面配置"""
|
||||||
import re
|
|
||||||
|
|
||||||
# 点击图标按钮打开查询界面
|
|
||||||
inner_frame.locator(".search-name-wrapper > .iconfont").click()
|
inner_frame.locator(".search-name-wrapper > .iconfont").click()
|
||||||
inner_frame.get_by_text("订单号查询").click()
|
inner_frame.get_by_text("订单号查询").click()
|
||||||
inner_frame.get_by_role("tab", name="全部").click()
|
inner_frame.get_by_role("tab", name="全部").click()
|
||||||
|
|
||||||
|
# 填充每页显示条数(5000条测试值)
|
||||||
|
input_el = inner_frame.locator("#rc_select_0")
|
||||||
|
input_el.fill("5000")
|
||||||
|
input_el.press("Enter")
|
||||||
|
|
||||||
# 填充并验证,如果失败则重试
|
def clean(self, production_id_file):
|
||||||
max_retries = 3
|
"""执行完整清理流程"""
|
||||||
expected_value = "5000"
|
# 0. 预加载数据库数据
|
||||||
for attempt in range(max_retries):
|
self.preload_data()
|
||||||
inner_frame.locator("#rc_select_0").fill(expected_value)
|
|
||||||
inner_frame.locator("#rc_select_0").press("Enter")
|
|
||||||
# 检查填充是否成功
|
|
||||||
actual_value = inner_frame.locator("#rc_select_0").input_value()
|
|
||||||
if actual_value == expected_value:
|
|
||||||
self._print(f"文本框填充成功: {expected_value}")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self._print(
|
|
||||||
f"第 {attempt + 1} 次填充失败,实际值: {actual_value},重试..."
|
|
||||||
)
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
self._print(
|
|
||||||
f"警告: {max_retries} 次尝试后仍未成功填充,继续执行..."
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean(self, production_id_file, debug_mode=False, debug_order=None):
|
|
||||||
"""
|
|
||||||
执行完整的数据清理流程
|
|
||||||
|
|
||||||
Args:
|
|
||||||
production_id_file: ProductionID.txt 文件路径
|
|
||||||
debug_mode: 是否启用调试模式
|
|
||||||
debug_order: 调试订单索引
|
|
||||||
"""
|
|
||||||
self._print(f"使用负责人 [{self.manager_name}] 进行数据清理")
|
|
||||||
|
|
||||||
with sync_playwright() as playwright:
|
with sync_playwright() as playwright:
|
||||||
# 调用登录模块
|
|
||||||
browser, context, page, main_frame = login(
|
browser, context, page, main_frame = login(
|
||||||
playwright=playwright,
|
playwright=playwright,
|
||||||
username=self.username,
|
username=self.username,
|
||||||
@@ -313,73 +190,51 @@ class DiscreteMaterialPlanCleaner:
|
|||||||
ignore_https_errors=True,
|
ignore_https_errors=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._print("=" * 80)
|
self._log("="*30 + " 开始清理任务 " + "="*30)
|
||||||
self._print("开始执行离散备料计划维护数据清理")
|
|
||||||
self._print("=" * 80)
|
# 进入功能页面
|
||||||
|
|
||||||
# 登录成功后可以进行后续操作
|
|
||||||
# 点击打开"功能菜单"
|
|
||||||
main_frame.locator("i").first.click()
|
main_frame.locator("i").first.click()
|
||||||
|
|
||||||
# 点击打开"离散生产订单维护"
|
|
||||||
with page.expect_popup() as page1_info:
|
with page.expect_popup() as page1_info:
|
||||||
main_frame.get_by_title("离散生产订单维护", exact=True).first.click()
|
main_frame.get_by_title("离散生产订单维护", exact=True).first.click()
|
||||||
page1 = page1_info.value
|
page1 = page1_info.value
|
||||||
|
|
||||||
# 获取 nested iframe
|
# 定位主 Iframe
|
||||||
main_frame = page1.locator("#forwardFrame").content_frame
|
work_main_frame = page1.locator("#forwardFrame").content_frame
|
||||||
inner_frame_locator = main_frame.locator("#mainiframe")
|
inner_frame = work_main_frame.locator("#mainiframe").content_frame
|
||||||
inner_frame_locator.wait_for(state="visible", timeout=15000)
|
inner_frame.locator("#hot-key-head_list").wait_for(state="visible", timeout=15000)
|
||||||
inner_frame = inner_frame_locator.content_frame
|
|
||||||
|
|
||||||
# 设置查询界面
|
|
||||||
self.setup_query_interface(inner_frame)
|
self.setup_query_interface(inner_frame)
|
||||||
|
|
||||||
# 读取总排号并查询生产订单号
|
|
||||||
order_ids = self.get_production_order_numbers(production_id_file)
|
order_ids = self.get_production_order_numbers(production_id_file)
|
||||||
|
|
||||||
# 按订单清理
|
# 遍历处理
|
||||||
for order_index, order_id in enumerate(order_ids):
|
for index, order_id in enumerate(order_ids):
|
||||||
self._print(
|
self._log(f"进度: [{index+1}/{len(order_ids)}] 处理单号: {order_id}")
|
||||||
f"\n=== 开始处理第 {order_index + 1} 个订单,订单号: {order_id} ==="
|
try:
|
||||||
)
|
self.process_order(inner_frame, order_id, index, page1)
|
||||||
self.process_order(
|
except Exception as e:
|
||||||
inner_frame,
|
self._log(f"处理单号 {order_id} 时发生异常: {e}", "error")
|
||||||
order_id,
|
continue # 单个失败不影响整体执行
|
||||||
order_index,
|
|
||||||
page1,
|
|
||||||
self.manager_name,
|
|
||||||
debug_mode=debug_mode,
|
|
||||||
debug_order=debug_order,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 执行账号注销
|
# 登出清理
|
||||||
self._print("\n开始执行账号注销...")
|
logout(work_main_frame, verbose=self.verbose)
|
||||||
logout(main_frame, verbose=self.verbose)
|
|
||||||
|
|
||||||
self._print(f"\n=== 全部完成 ===")
|
|
||||||
|
|
||||||
# 关闭浏览器
|
|
||||||
context.close()
|
context.close()
|
||||||
browser.close()
|
browser.close()
|
||||||
|
self._log("="*30 + " 任务全部完成 " + "="*30)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""测试函数"""
|
# 路径配置
|
||||||
|
base_dir = os.path.dirname(__file__)
|
||||||
|
id_file = os.path.join(base_dir, "productionID.txt")
|
||||||
|
|
||||||
cleaner = DiscreteMaterialPlanCleaner(
|
cleaner = DiscreteMaterialPlanCleaner(
|
||||||
username="BLDpengqiangqiang",
|
username="BLDpengqiangqiang",
|
||||||
password="Cqbld123456.",
|
password="your_password_here",
|
||||||
manager_name="彭羽",
|
manager_name="彭羽",
|
||||||
headless=False,
|
headless=False
|
||||||
verbose=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
production_id_file = os.path.join(os.path.dirname(__file__), "productionID.txt")
|
cleaner.clean(id_file)
|
||||||
|
input("执行完毕,按回车键退出程序...")
|
||||||
cleaner.clean(production_id_file)
|
|
||||||
|
|
||||||
input("按回车退出...")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in New Issue
Block a user