style: format all Python files with Black

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

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

View File

@@ -1,6 +1,7 @@
""" """
Auth package for user authentication and session management Auth package for user authentication and session management
""" """
from .session_manager import SessionManager from .session_manager import SessionManager
__all__ = ['SessionManager'] __all__ = ["SessionManager"]

View File

@@ -1,6 +1,7 @@
""" """
Session Manager - Singleton pattern for managing authenticated user session Session Manager - Singleton pattern for managing authenticated user session
""" """
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
@@ -28,7 +29,7 @@ class SessionManager:
self._initialized = True self._initialized = True
@classmethod @classmethod
def get_instance(cls) -> 'SessionManager': def get_instance(cls) -> "SessionManager":
""" """
Get the singleton instance of SessionManager Get the singleton instance of SessionManager
@@ -57,8 +58,8 @@ class SessionManager:
if user_info: if user_info:
self._current_user = { self._current_user = {
'username': user_info['username'], "username": user_info["username"],
'user_type': user_info['user_type'] "user_type": user_info["user_type"],
} }
return True return True
return False return False
@@ -79,8 +80,8 @@ class SessionManager:
if user_info: if user_info:
self._current_user = { self._current_user = {
'username': user_info['username'], "username": user_info["username"],
'user_type': user_info['user_type'] "user_type": user_info["user_type"],
} }
return True return True
return False return False
@@ -107,7 +108,7 @@ class SessionManager:
""" """
if not self._current_user: if not self._current_user:
return False return False
return self._current_user.get('user_type') == 'Admin' return self._current_user.get("user_type") == "Admin"
def is_guest(self) -> bool: def is_guest(self) -> bool:
""" """
@@ -118,7 +119,7 @@ class SessionManager:
""" """
if not self._current_user: if not self._current_user:
return False return False
return self._current_user.get('user_type') == 'Guest' return self._current_user.get("user_type") == "Guest"
def get_username(self) -> Optional[str]: def get_username(self) -> Optional[str]:
""" """
@@ -129,7 +130,7 @@ class SessionManager:
""" """
if not self._current_user: if not self._current_user:
return None return None
return self._current_user.get('username') return self._current_user.get("username")
def get_user_type(self) -> Optional[str]: def get_user_type(self) -> Optional[str]:
""" """
@@ -140,7 +141,7 @@ class SessionManager:
""" """
if not self._current_user: if not self._current_user:
return None return None
return self._current_user.get('user_type') return self._current_user.get("user_type")
def get_user_info(self) -> Optional[dict]: def get_user_info(self) -> Optional[dict]:
""" """
@@ -168,12 +169,12 @@ class SessionManager:
return False return False
# Store original admin user for reference # Store original admin user for reference
if not hasattr(self, '_original_admin_user'): if not hasattr(self, "_original_admin_user"):
self._original_admin_user = self._current_user.copy() self._original_admin_user = self._current_user.copy()
self._current_user = { self._current_user = {
'username': user_info['username'], "username": user_info["username"],
'user_type': user_info['user_type'] "user_type": user_info["user_type"],
} }
return True return True
@@ -184,4 +185,4 @@ class SessionManager:
Returns: Returns:
Original admin user dict if a switch occurred, None otherwise Original admin user dict if a switch occurred, None otherwise
""" """
return getattr(self, '_original_admin_user', None) return getattr(self, "_original_admin_user", None)

View File

@@ -5,6 +5,7 @@
定义所有配置项的默认值,从环境变量加载。 定义所有配置项的默认值,从环境变量加载。
""" """
from config.schema import ( from config.schema import (
ERPConfig, ERPConfig,
DatabaseConfig, DatabaseConfig,
@@ -17,7 +18,6 @@ from config.schema import (
DatabaseType, DatabaseType,
) )
# 默认配置 - 从环境变量加载 # 默认配置 - 从环境变量加载
DEFAULT_APP_CONFIG = AppConfig.from_env() DEFAULT_APP_CONFIG = AppConfig.from_env()

View File

@@ -5,6 +5,7 @@
使用 python-dotenv 加载 .env 文件,并提供类型转换功能。 使用 python-dotenv 加载 .env 文件,并提供类型转换功能。
""" """
import os import os
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Type, TypeVar from typing import Any, Optional, Type, TypeVar
@@ -111,7 +112,9 @@ def set_env(key: str, value: Any) -> None:
os.environ[key] = str(value) os.environ[key] = str(value)
def save_env_file(env_file: Optional[str] = None, env_dict: Optional[dict] = None) -> bool: def save_env_file(
env_file: Optional[str] = None, env_dict: Optional[dict] = None
) -> bool:
""" """
保存环境变量到 .env 文件 保存环境变量到 .env 文件

View File

@@ -5,6 +5,7 @@
负责加载、合并和验证配置,优先从环境变量加载。 负责加载、合并和验证配置,优先从环境变量加载。
""" """
import json import json
import os import os
from typing import Any, Dict from typing import Any, Dict
@@ -27,7 +28,9 @@ class ConfigLoader:
"""配置加载器""" """配置加载器"""
@staticmethod @staticmethod
def load(config_file: str = "config/user_settings.json", use_env: bool = True) -> AppConfig: def load(
config_file: str = "config/user_settings.json", use_env: bool = True
) -> AppConfig:
""" """
加载配置 加载配置
@@ -111,18 +114,36 @@ class ConfigLoader:
"ERP_IGNORE_HTTPS_ERRORS": config.erp.ignore_https_errors, "ERP_IGNORE_HTTPS_ERRORS": config.erp.ignore_https_errors,
"ERP_AUTO_CLOSE_BROWSER": config.erp.auto_close_browser, "ERP_AUTO_CLOSE_BROWSER": config.erp.auto_close_browser,
# 数据库配置 # 数据库配置
"DB_TYPE": config.database.db_type.value if isinstance(config.database.db_type, DatabaseType) else config.database.db_type, "DB_TYPE": (
config.database.db_type.value
if isinstance(config.database.db_type, DatabaseType)
else config.database.db_type
),
"DB_SERVER": config.database.server, "DB_SERVER": config.database.server,
"DB_NAME": config.database.database, "DB_NAME": config.database.database,
"DB_USERNAME": config.database.username, "DB_USERNAME": config.database.username,
"DB_PASSWORD": config.database.password, "DB_PASSWORD": config.database.password,
# SQL Server 特定配置 # SQL Server 特定配置
"DB_SQLSERVER_DRIVER": config.database.sqlserver.driver if config.database.sqlserver else "ODBC Driver 18 for SQL Server", "DB_SQLSERVER_DRIVER": (
"DB_TRUST_SERVER_CERTIFICATE": config.database.sqlserver.trust_server_certificate if config.database.sqlserver else "yes", config.database.sqlserver.driver
if config.database.sqlserver
else "ODBC Driver 18 for SQL Server"
),
"DB_TRUST_SERVER_CERTIFICATE": (
config.database.sqlserver.trust_server_certificate
if config.database.sqlserver
else "yes"
),
# MySQL 特定配置 # MySQL 特定配置
"DB_MYSQL_HOST": config.database.mysql.host if config.database.mysql else "", "DB_MYSQL_HOST": (
"DB_MYSQL_PORT": config.database.mysql.port if config.database.mysql else 3306, config.database.mysql.host if config.database.mysql else ""
"DB_MYSQL_CHARSET": config.database.mysql.charset if config.database.mysql else "utf8mb4", ),
"DB_MYSQL_PORT": (
config.database.mysql.port if config.database.mysql else 3306
),
"DB_MYSQL_CHARSET": (
config.database.mysql.charset if config.database.mysql else "utf8mb4"
),
# 路径配置 # 路径配置
"PATH_DATA_DIR": config.paths.data_dir, "PATH_DATA_DIR": config.paths.data_dir,
"PATH_PRODUCTION_ID_FILE": config.paths.production_id_file, "PATH_PRODUCTION_ID_FILE": config.paths.production_id_file,
@@ -199,7 +220,9 @@ class ConfigLoader:
sqlserver_dict = database_dict.get("sqlserver", {}) sqlserver_dict = database_dict.get("sqlserver", {})
sqlserver_config = SQLServerConfig( sqlserver_config = SQLServerConfig(
driver=sqlserver_dict.get("driver", "ODBC Driver 18 for SQL Server"), driver=sqlserver_dict.get("driver", "ODBC Driver 18 for SQL Server"),
trust_server_certificate=sqlserver_dict.get("trust_server_certificate", "yes"), trust_server_certificate=sqlserver_dict.get(
"trust_server_certificate", "yes"
),
) )
# 解析 MySQL 配置 # 解析 MySQL 配置
@@ -243,16 +266,18 @@ class ConfigLoader:
verbose=extraction_dict.get("verbose", True), verbose=extraction_dict.get("verbose", True),
auto_convert=extraction_dict.get("auto_convert", True), auto_convert=extraction_dict.get("auto_convert", True),
merge_batches=extraction_dict.get("merge_batches", True), merge_batches=extraction_dict.get("merge_batches", True),
enable_db_persistence=extraction_dict.get("enable_db_persistence", False), enable_db_persistence=extraction_dict.get(
"enable_db_persistence", False
),
), ),
validation=ValidationConfig( validation=ValidationConfig(
data_source=validation_dict.get("data_source", "database_full"), data_source=validation_dict.get("data_source", "database_full"),
use_database=validation_dict.get("use_database", True), use_database=validation_dict.get("use_database", True),
batch_size=validation_dict.get("batch_size", 2000), batch_size=validation_dict.get("batch_size", 2000),
enable_crud_operations=validation_dict.get("enable_crud_operations", False), enable_crud_operations=validation_dict.get(
"enable_crud_operations", False
),
default_manager=validation_dict.get("default_manager", ""), default_manager=validation_dict.get("default_manager", ""),
match_mode=validation_dict.get("match_mode", "substring"), match_mode=validation_dict.get("match_mode", "substring"),
), ),
) )

View File

@@ -5,6 +5,7 @@
使用 dataclass 定义所有配置项的结构和类型。 使用 dataclass 定义所有配置项的结构和类型。
""" """
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional
from pathlib import Path from pathlib import Path
@@ -13,6 +14,7 @@ from enum import Enum
class DatabaseType(str, Enum): class DatabaseType(str, Enum):
"""数据库类型枚举""" """数据库类型枚举"""
SQLSERVER = "sqlserver" SQLSERVER = "sqlserver"
MYSQL = "mysql" MYSQL = "mysql"
@@ -57,6 +59,7 @@ class ERPConfig:
@dataclass @dataclass
class SQLServerConfig: class SQLServerConfig:
"""SQL Server 特定配置""" """SQL Server 特定配置"""
driver: str = "ODBC Driver 18 for SQL Server" driver: str = "ODBC Driver 18 for SQL Server"
trust_server_certificate: str = "yes" trust_server_certificate: str = "yes"
@@ -74,6 +77,7 @@ class SQLServerConfig:
@dataclass @dataclass
class MySQLConfig: class MySQLConfig:
"""MySQL 特定配置""" """MySQL 特定配置"""
host: str = "" host: str = ""
port: int = 3306 port: int = 3306
charset: str = "utf8mb4" charset: str = "utf8mb4"
@@ -167,7 +171,9 @@ class PathConfig:
data_dir=get_env("PATH_DATA_DIR", "D:/python/playwrite/data/"), data_dir=get_env("PATH_DATA_DIR", "D:/python/playwrite/data/"),
production_id_file=get_env("PATH_PRODUCTION_ID_FILE", "ProductionID.txt"), production_id_file=get_env("PATH_PRODUCTION_ID_FILE", "ProductionID.txt"),
default_output=get_env("PATH_DEFAULT_OUTPUT", "离散备料计划维护_合并.xlsx"), default_output=get_env("PATH_DEFAULT_OUTPUT", "离散备料计划维护_合并.xlsx"),
validation_output=get_env("PATH_VALIDATION_OUTPUT", "物料状态校验结果.xlsx"), validation_output=get_env(
"PATH_VALIDATION_OUTPUT", "物料状态校验结果.xlsx"
),
) )
def validate(self) -> list[str]: def validate(self) -> list[str]:
@@ -200,7 +206,9 @@ class ExtractionConfig:
verbose=get_env_bool("EXTRACTION_VERBOSE", True), verbose=get_env_bool("EXTRACTION_VERBOSE", True),
auto_convert=get_env_bool("EXTRACTION_AUTO_CONVERT", True), auto_convert=get_env_bool("EXTRACTION_AUTO_CONVERT", True),
merge_batches=get_env_bool("EXTRACTION_MERGE_BATCHES", True), merge_batches=get_env_bool("EXTRACTION_MERGE_BATCHES", True),
enable_db_persistence=get_env_bool("EXTRACTION_ENABLE_DB_PERSISTENCE", False), enable_db_persistence=get_env_bool(
"EXTRACTION_ENABLE_DB_PERSISTENCE", False
),
) )
def validate(self) -> list[str]: def validate(self) -> list[str]:
@@ -246,7 +254,7 @@ class ValidationConfig:
"database_full", "database_full",
"database_filtered", "database_filtered",
"excel_existing", "excel_existing",
"excel_full" "excel_full",
] ]
if self.data_source not in valid_sources: if self.data_source not in valid_sources:
errors.append( errors.append(
@@ -281,6 +289,7 @@ class UIConfig:
def from_env(cls) -> "UIConfig": def from_env(cls) -> "UIConfig":
"""从环境变量创建配置""" """从环境变量创建配置"""
from config.env_loader import get_env, get_env_int from config.env_loader import get_env, get_env_int
return cls( return cls(
font_family=get_env("UI_FONT_FAMILY", "Microsoft YaHei UI"), font_family=get_env("UI_FONT_FAMILY", "Microsoft YaHei UI"),
font_size=get_env_int("UI_FONT_SIZE", 10), font_size=get_env_int("UI_FONT_SIZE", 10),
@@ -366,19 +375,35 @@ class AppConfig:
"auto_close_browser": self.erp.auto_close_browser, "auto_close_browser": self.erp.auto_close_browser,
}, },
"database": { "database": {
"db_type": self.database.db_type if isinstance(self.database.db_type, str) else self.database.db_type.value, "db_type": (
self.database.db_type
if isinstance(self.database.db_type, str)
else self.database.db_type.value
),
"server": self.database.server, "server": self.database.server,
"database": self.database.database, "database": self.database.database,
"username": self.database.username, "username": self.database.username,
"password": self.database.password, "password": self.database.password,
"sqlserver": { "sqlserver": {
"driver": self.database.sqlserver.driver if self.database.sqlserver else "ODBC Driver 18 for SQL Server", "driver": (
"trust_server_certificate": self.database.sqlserver.trust_server_certificate if self.database.sqlserver else "yes", self.database.sqlserver.driver
if self.database.sqlserver
else "ODBC Driver 18 for SQL Server"
),
"trust_server_certificate": (
self.database.sqlserver.trust_server_certificate
if self.database.sqlserver
else "yes"
),
}, },
"mysql": { "mysql": {
"host": self.database.mysql.host if self.database.mysql else "", "host": self.database.mysql.host if self.database.mysql else "",
"port": self.database.mysql.port if self.database.mysql else 3306, "port": self.database.mysql.port if self.database.mysql else 3306,
"charset": self.database.mysql.charset if self.database.mysql else "utf8mb4", "charset": (
self.database.mysql.charset
if self.database.mysql
else "utf8mb4"
),
}, },
}, },
"paths": { "paths": {

View File

@@ -37,7 +37,9 @@ class BaseDatabaseConnection(ABC):
pass pass
@abstractmethod @abstractmethod
def execute_query(self, sql: str, params: Optional[tuple] = None) -> List[Dict[str, Any]]: def execute_query(
self, sql: str, params: Optional[tuple] = None
) -> List[Dict[str, Any]]:
""" """
执行查询语句并返回结果 执行查询语句并返回结果

View File

@@ -19,6 +19,7 @@ class BaseDAO:
self.db: Optional[BaseDatabaseConnection] = None self.db: Optional[BaseDatabaseConnection] = None
# 从配置文件加载数据库类型 # 从配置文件加载数据库类型
from config.loader import ConfigLoader from config.loader import ConfigLoader
app_config = ConfigLoader.load() app_config = ConfigLoader.load()
self._db_type = app_config.database.db_type self._db_type = app_config.database.db_type
@@ -50,7 +51,7 @@ class BaseDAO:
""" """
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
# SQL Server → MySQL # SQL Server → MySQL
return TableNameConverter.convert_sql(sql, 'mysql') return TableNameConverter.convert_sql(sql, "mysql")
return sql return sql
def _get_placeholder(self) -> str: def _get_placeholder(self) -> str:

View File

@@ -1,6 +1,7 @@
""" """
BIPUsers DAO - Data access object for user authentication and management BIPUsers DAO - Data access object for user authentication and management
""" """
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
from db.base_dao import BaseDAO from db.base_dao import BaseDAO
from db.connection import get_connection from db.connection import get_connection
@@ -22,7 +23,7 @@ class BIPUsersDAO(BaseDAO):
Dict with user info if authentication successful, None otherwise Dict with user info if authentication successful, None otherwise
Returns: {id, username, user_type} Returns: {id, username, user_type}
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -43,13 +44,15 @@ class BIPUsersDAO(BaseDAO):
results = db.execute_query(sql, (username, password)) results = db.execute_query(sql, (username, password))
if results: if results:
return { return {
'id': results[0]['ID'], "id": results[0]["ID"],
'username': results[0]['UserName'], "username": results[0]["UserName"],
'user_type': results[0]['UserType'] "user_type": results[0]["UserType"],
} }
return None return None
def authenticate_by_computer_name(self, computer_name: str) -> Optional[Dict[str, Any]]: def authenticate_by_computer_name(
self, computer_name: str
) -> Optional[Dict[str, Any]]:
""" """
Authenticate a user using computer name (silent login) Authenticate a user using computer name (silent login)
@@ -60,7 +63,7 @@ class BIPUsersDAO(BaseDAO):
Dict with user info if authentication successful, None otherwise Dict with user info if authentication successful, None otherwise
Returns: {id, username, user_type} Returns: {id, username, user_type}
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# Note: Column name is 'ComputerNmae' (typo in database schema) # Note: Column name is 'ComputerNmae' (typo in database schema)
@@ -81,9 +84,9 @@ class BIPUsersDAO(BaseDAO):
results = db.execute_query(sql, (computer_name,)) results = db.execute_query(sql, (computer_name,))
if results: if results:
return { return {
'id': results[0]['ID'], "id": results[0]["ID"],
'username': results[0]['UserName'], "username": results[0]["UserName"],
'user_type': results[0]['UserType'] "user_type": results[0]["UserType"],
} }
return None return None
@@ -94,7 +97,7 @@ class BIPUsersDAO(BaseDAO):
Returns: Returns:
List of user dictionaries: [{id, username, user_type, create_time}] List of user dictionaries: [{id, username, user_type, create_time}]
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -114,15 +117,17 @@ class BIPUsersDAO(BaseDAO):
results = db.execute_query(sql) results = db.execute_query(sql)
return [ return [
{ {
'id': row['ID'], "id": row["ID"],
'username': row['UserName'], "username": row["UserName"],
'user_type': row['UserType'], "user_type": row["UserType"],
'create_time': row['CreateTime'] "create_time": row["CreateTime"],
} }
for row in results for row in results
] ]
def create_user(self, username: str, password: str, user_type: str, computer_name: str = '') -> bool: def create_user(
self, username: str, password: str, user_type: str, computer_name: str = ""
) -> bool:
""" """
Create a new user with optional computer name for silent login Create a new user with optional computer name for silent login
@@ -135,7 +140,7 @@ class BIPUsersDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -185,7 +190,7 @@ class BIPUsersDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -221,7 +226,7 @@ class BIPUsersDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -256,7 +261,7 @@ class BIPUsersDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -289,7 +294,7 @@ class BIPUsersDAO(BaseDAO):
Returns: Returns:
True if username exists, False otherwise True if username exists, False otherwise
""" """
table_name = self._convert_sql('[dbo].[BIPUsers]') table_name = self._convert_sql("[dbo].[BIPUsers]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -306,4 +311,4 @@ class BIPUsersDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
results = db.execute_query(sql, (username,)) results = db.execute_query(sql, (username,))
return results[0]['count'] > 0 if results else False return results[0]["count"] > 0 if results else False

View File

@@ -34,6 +34,7 @@ def get_connection(config=None) -> BaseDatabaseConnection:
else: else:
# 从用户配置文件加载 # 从用户配置文件加载
from config.loader import ConfigLoader from config.loader import ConfigLoader
app_config = ConfigLoader.load() app_config = ConfigLoader.load()
database_config = app_config.database database_config = app_config.database

View File

@@ -16,8 +16,7 @@ class ConnectionFactory:
@staticmethod @staticmethod
def create_connection( def create_connection(
db_type: DatabaseType, db_type: DatabaseType, config: Optional[Dict[str, Any]] = None
config: Optional[Dict[str, Any]] = None
) -> BaseDatabaseConnection: ) -> BaseDatabaseConnection:
""" """
根据数据库类型创建对应的连接实例 根据数据库类型创建对应的连接实例
@@ -58,14 +57,14 @@ class ConnectionFactory:
if db_type == DatabaseType.SQLSERVER: if db_type == DatabaseType.SQLSERVER:
# 构建 SQL Server 配置字典 # 构建 SQL Server 配置字典
config = { config = {
'server': database_config.server, "server": database_config.server,
'database': database_config.database, "database": database_config.database,
'username': database_config.username, "username": database_config.username,
'password': database_config.password, "password": database_config.password,
} }
if database_config.sqlserver: if database_config.sqlserver:
config['driver'] = database_config.sqlserver.driver config["driver"] = database_config.sqlserver.driver
config['trust_server_certificate'] = ( config["trust_server_certificate"] = (
database_config.sqlserver.trust_server_certificate database_config.sqlserver.trust_server_certificate
) )
return SQLServerConnection(config) return SQLServerConnection(config)
@@ -73,17 +72,17 @@ class ConnectionFactory:
elif db_type == DatabaseType.MYSQL: elif db_type == DatabaseType.MYSQL:
# 构建 MySQL 配置字典 # 构建 MySQL 配置字典
config = { config = {
'database': database_config.database, "database": database_config.database,
'username': database_config.username, "username": database_config.username,
'password': database_config.password, "password": database_config.password,
} }
if database_config.mysql: if database_config.mysql:
config['host'] = database_config.mysql.host config["host"] = database_config.mysql.host
config['port'] = database_config.mysql.port config["port"] = database_config.mysql.port
config['charset'] = database_config.mysql.charset config["charset"] = database_config.mysql.charset
else: else:
# 回退到 server 字段(兼容旧配置) # 回退到 server 字段(兼容旧配置)
config['host'] = database_config.server config["host"] = database_config.server
return MySQLConnection(config) return MySQLConnection(config)
else: else:

View File

@@ -37,19 +37,21 @@ class DiscreteMaterialPlanDAO(BaseDAO):
... print(f"Deleted: {stats['deleted']}, Inserted: {stats['inserted']}") ... print(f"Deleted: {stats['deleted']}, Inserted: {stats['inserted']}")
""" """
if df.empty: if df.empty:
return {'deleted': 0, 'inserted': 0} return {"deleted": 0, "inserted": 0}
# Remove duplicates based on PlanNumber and SequenceNumber # Remove duplicates based on PlanNumber and SequenceNumber
original_count = len(df) original_count = len(df)
df = df.drop_duplicates(subset=['备料计划单号', '序号'], keep='first') df = df.drop_duplicates(subset=["备料计划单号", "序号"], keep="first")
duplicates_removed = original_count - len(df) duplicates_removed = original_count - len(df)
if duplicates_removed > 0: if duplicates_removed > 0:
print(f"[INFO] 检测到 {duplicates_removed} 条重复记录(相同计划单号和序号),已自动去重") print(
f"[INFO] 检测到 {duplicates_removed} 条重复记录(相同计划单号和序号),已自动去重"
)
with get_connection() as db: with get_connection() as db:
# Get unique plan numbers # Get unique plan numbers
plan_numbers = df['备料计划单号'].unique().tolist() plan_numbers = df["备料计划单号"].unique().tolist()
# Delete existing records # Delete existing records
deleted = self._delete_by_plan_numbers(db, plan_numbers) deleted = self._delete_by_plan_numbers(db, plan_numbers)
@@ -57,7 +59,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
# Insert new records in batches # Insert new records in batches
inserted = self._batch_insert(db, df) inserted = self._batch_insert(db, df)
return {'deleted': deleted, 'inserted': inserted} return {"deleted": deleted, "inserted": inserted}
def _delete_by_plan_numbers(self, db, plan_numbers: List[str]) -> int: def _delete_by_plan_numbers(self, db, plan_numbers: List[str]) -> int:
""" """
@@ -79,12 +81,12 @@ class DiscreteMaterialPlanDAO(BaseDAO):
total_deleted = 0 total_deleted = 0
for i in range(0, len(plan_numbers), batch_size): for i in range(0, len(plan_numbers), batch_size):
batch = plan_numbers[i:i + batch_size] batch = plan_numbers[i : i + batch_size]
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch]) placeholders = ",".join([placeholder for _ in batch])
# 根据数据库类型选择表名 # 根据数据库类型选择表名
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f"DELETE FROM {table_name} WHERE PlanNumber IN ({placeholders})" sql = f"DELETE FROM {table_name} WHERE PlanNumber IN ({placeholders})"
deleted = db.execute_update(sql, tuple(batch)) deleted = db.execute_update(sql, tuple(batch))
@@ -108,7 +110,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
Total number of records inserted Total number of records inserted
""" """
# 根据数据库类型选择表名 # 根据数据库类型选择表名
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
sql = f""" sql = f"""
@@ -126,7 +128,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
records = self._convert_df_to_records(df) records = self._convert_df_to_records(df)
for i in range(0, len(records), batch_size): for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size] batch = records[i : i + batch_size]
for record in batch: for record in batch:
db.execute_update(sql, record) db.execute_update(sql, record)
total_inserted += 1 total_inserted += 1
@@ -150,20 +152,44 @@ class DiscreteMaterialPlanDAO(BaseDAO):
""" """
# Column order must match INSERT statement # Column order must match INSERT statement
column_order = [ column_order = [
'工厂', '备料状态', '备料计划单号', '来源单号', '备料类型', '产品编码', "工厂",
'产品名称', '产品单位', '产品计划数量', '用料部门', '备注', '制单人', "备料状态",
'制单日期', '审批人', '审批日期', '序号', '材料编码', '材料名称', "备料计划单号",
'规格', '型号', '图号', '物料材质', '计划数量', '单位', '需用日期', "来源单号",
'发料仓库', '单位用量', '累计出库数量', 'BOM版本' "备料类型",
"产品编码",
"产品名称",
"产品单位",
"产品计划数量",
"用料部门",
"备注",
"制单人",
"制单日期",
"审批人",
"审批日期",
"序号",
"材料编码",
"材料名称",
"规格",
"型号",
"图号",
"物料材质",
"计划数量",
"单位",
"需用日期",
"发料仓库",
"单位用量",
"累计出库数量",
"BOM版本",
] ]
# Numeric columns with their default values and data types # Numeric columns with their default values and data types
numeric_columns = { numeric_columns = {
'产品计划数量': (0, int), "产品计划数量": (0, int),
'序号': (0, int), "序号": (0, int),
'计划数量': (0, int), "计划数量": (0, int),
'单位用量': (0.0, float), "单位用量": (0.0, float),
'累计出库数量': (0, int), "累计出库数量": (0, int),
} }
records = [] records = []
@@ -172,7 +198,11 @@ class DiscreteMaterialPlanDAO(BaseDAO):
for col in column_order: for col in column_order:
value = row.get(col) value = row.get(col)
# Handle NaN, None, or empty string values # Handle NaN, None, or empty string values
if pd.isna(value) or value is None or (isinstance(value, str) and value.strip() == ''): if (
pd.isna(value)
or value is None
or (isinstance(value, str) and value.strip() == "")
):
if col in numeric_columns: if col in numeric_columns:
# Use default value for numeric columns # Use default value for numeric columns
record.append(numeric_columns[col][0]) record.append(numeric_columns[col][0])
@@ -209,7 +239,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
List of dictionaries representing records List of dictionaries representing records
""" """
with get_connection() as db: with get_connection() as db:
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
sql = f"SELECT * FROM {table_name} WHERE PlanNumber = {placeholder}" sql = f"SELECT * FROM {table_name} WHERE PlanNumber = {placeholder}"
return db.execute_query(sql, (plan_number,)) return db.execute_query(sql, (plan_number,))
@@ -227,8 +257,8 @@ class DiscreteMaterialPlanDAO(BaseDAO):
if not plan_numbers: if not plan_numbers:
return [] return []
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in plan_numbers]) placeholders = ",".join([placeholder for _ in plan_numbers])
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f"SELECT * FROM {table_name} WHERE PlanNumber IN ({placeholders})" sql = f"SELECT * FROM {table_name} WHERE PlanNumber IN ({placeholders})"
with get_connection() as db: with get_connection() as db:
return db.execute_query(sql, tuple(plan_numbers)) return db.execute_query(sql, tuple(plan_numbers))
@@ -244,7 +274,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
List of dictionaries representing records List of dictionaries representing records
""" """
with get_connection() as db: with get_connection() as db:
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
sql = f"SELECT * FROM {table_name} WHERE SourceNumber = {placeholder}" sql = f"SELECT * FROM {table_name} WHERE SourceNumber = {placeholder}"
return db.execute_query(sql, (order_id,)) return db.execute_query(sql, (order_id,))
@@ -260,11 +290,11 @@ class DiscreteMaterialPlanDAO(BaseDAO):
Number of records Number of records
""" """
with get_connection() as db: with get_connection() as db:
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
sql = f"SELECT COUNT(*) as count FROM {table_name} WHERE PlanNumber = {placeholder}" sql = f"SELECT COUNT(*) as count FROM {table_name} WHERE PlanNumber = {placeholder}"
result = db.execute_query(sql, (plan_number,)) result = db.execute_query(sql, (plan_number,))
return result[0]['count'] if result else 0 return result[0]["count"] if result else 0
def count_all(self) -> int: def count_all(self) -> int:
""" """
@@ -274,10 +304,10 @@ class DiscreteMaterialPlanDAO(BaseDAO):
Total number of records Total number of records
""" """
with get_connection() as db: with get_connection() as db:
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f"SELECT COUNT(*) as count FROM {table_name}" sql = f"SELECT COUNT(*) as count FROM {table_name}"
result = db.execute_query(sql) result = db.execute_query(sql)
return result[0]['count'] if result else 0 return result[0]["count"] if result else 0
def delete_by_plan_numbers(self, plan_numbers: List[str]) -> int: def delete_by_plan_numbers(self, plan_numbers: List[str]) -> int:
""" """
@@ -301,7 +331,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
unique plans, unique orders, and date range unique plans, unique orders, and date range
""" """
with get_connection() as db: with get_connection() as db:
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f""" sql = f"""
SELECT SELECT
COUNT(*) as total_records, COUNT(*) as total_records,
@@ -324,7 +354,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
List of dictionaries representing all records List of dictionaries representing all records
""" """
with get_connection() as db: with get_connection() as db:
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f"SELECT * FROM {table_name}" sql = f"SELECT * FROM {table_name}"
return db.execute_query(sql) return db.execute_query(sql)
@@ -346,10 +376,10 @@ class DiscreteMaterialPlanDAO(BaseDAO):
all_results = [] all_results = []
for i in range(0, len(source_numbers), batch_size): for i in range(0, len(source_numbers), batch_size):
batch = source_numbers[i:i + batch_size] batch = source_numbers[i : i + batch_size]
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch]) placeholders = ",".join([placeholder for _ in batch])
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f"SELECT * FROM {table_name} WHERE SourceNumber IN ({placeholders})" sql = f"SELECT * FROM {table_name} WHERE SourceNumber IN ({placeholders})"
with get_connection() as db: with get_connection() as db:
results = db.execute_query(sql, tuple(batch)) results = db.execute_query(sql, tuple(batch))
@@ -368,7 +398,7 @@ class DiscreteMaterialPlanDAO(BaseDAO):
List of dictionaries representing deduplicated records List of dictionaries representing deduplicated records
""" """
with get_connection() as db: with get_connection() as db:
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f""" sql = f"""
WITH RankedRecords AS ( WITH RankedRecords AS (
@@ -414,10 +444,10 @@ class DiscreteMaterialPlanDAO(BaseDAO):
all_results = [] all_results = []
for i in range(0, len(source_numbers), batch_size): for i in range(0, len(source_numbers), batch_size):
batch = source_numbers[i:i + batch_size] batch = source_numbers[i : i + batch_size]
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch]) placeholders = ",".join([placeholder for _ in batch])
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
sql = f""" sql = f"""
WITH RankedRecords AS ( WITH RankedRecords AS (
@@ -458,23 +488,23 @@ class DiscreteMaterialPlanDAO(BaseDAO):
Returns: Returns:
List of unique material names List of unique material names
""" """
table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]")
if source_numbers is None or not source_numbers: if source_numbers is None or not source_numbers:
# No filter - get all unique material names # No filter - get all unique material names
sql = f"SELECT DISTINCT MaterialName FROM {table_name} WHERE MaterialName IS NOT NULL" sql = f"SELECT DISTINCT MaterialName FROM {table_name} WHERE MaterialName IS NOT NULL"
with get_connection() as db: with get_connection() as db:
results = db.execute_query(sql) results = db.execute_query(sql)
return [r['MaterialName'] for r in results if r.get('MaterialName')] return [r["MaterialName"] for r in results if r.get("MaterialName")]
else: else:
# Filter by SourceNumber list # Filter by SourceNumber list
batch_size = 2000 batch_size = 2000
all_material_names = set() all_material_names = set()
for i in range(0, len(source_numbers), batch_size): for i in range(0, len(source_numbers), batch_size):
batch = source_numbers[i:i + batch_size] batch = source_numbers[i : i + batch_size]
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch]) placeholders = ",".join([placeholder for _ in batch])
sql = f""" sql = f"""
SELECT DISTINCT MaterialName SELECT DISTINCT MaterialName
FROM {table_name} FROM {table_name}
@@ -483,7 +513,9 @@ class DiscreteMaterialPlanDAO(BaseDAO):
""" """
with get_connection() as db: with get_connection() as db:
results = db.execute_query(sql, tuple(batch)) results = db.execute_query(sql, tuple(batch))
batch_materials = [r['MaterialName'] for r in results if r.get('MaterialName')] batch_materials = [
r["MaterialName"] for r in results if r.get("MaterialName")
]
all_material_names.update(batch_materials) all_material_names.update(batch_materials)
return list(all_material_names) return list(all_material_names)

View File

@@ -16,9 +16,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
# ==================== CREATE ==================== # ==================== CREATE ====================
def insert_material( def insert_material(self, material_name: str, manager_name: str) -> bool:
self, material_name: str, manager_name: str
) -> bool:
""" """
Insert a single material record. Insert a single material record.
@@ -29,7 +27,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -65,7 +63,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
if not materials: if not materials:
return 0 return 0
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -100,7 +98,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
List of all materials with MaterialName and ManagerName List of all materials with MaterialName and ManagerName
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -131,7 +129,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
List of materials for the specified manager List of materials for the specified manager
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -160,7 +158,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
List of unique manager names List of unique manager names
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -180,7 +178,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
results = db.execute_query(sql) results = db.execute_query(sql)
return [r['ManagerName'] for r in results if r.get('ManagerName')] return [r["ManagerName"] for r in results if r.get("ManagerName")]
def get_material_names_by_manager(self, manager_name: str) -> List[str]: def get_material_names_by_manager(self, manager_name: str) -> List[str]:
""" """
@@ -193,15 +191,12 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
List of material names for the specified manager List of material names for the specified manager
""" """
results = self.get_materials_by_manager(manager_name) results = self.get_materials_by_manager(manager_name)
return [r['MaterialName'] for r in results if r.get('MaterialName')] return [r["MaterialName"] for r in results if r.get("MaterialName")]
# ==================== UPDATE ==================== # ==================== UPDATE ====================
def update_manager( def update_manager(
self, self, material_name: str, old_manager: str, new_manager: str
material_name: str,
old_manager: str,
new_manager: str
) -> bool: ) -> bool:
""" """
Update manager for a specific material. Update manager for a specific material.
@@ -214,7 +209,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -233,7 +228,9 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
try: try:
with get_connection() as db: with get_connection() as db:
affected = db.execute_update(sql, (new_manager, material_name, old_manager)) affected = db.execute_update(
sql, (new_manager, material_name, old_manager)
)
return affected > 0 return affected > 0
except Exception as e: except Exception as e:
print(f"Error updating manager: {e}") print(f"Error updating manager: {e}")
@@ -241,11 +238,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
# ==================== DELETE ==================== # ==================== DELETE ====================
def delete_material( def delete_material(self, material_name: str, manager_name: str) -> bool:
self,
material_name: str,
manager_name: str
) -> bool:
""" """
Delete a specific material record. Delete a specific material record.
@@ -256,7 +249,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -289,7 +282,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
Number of records deleted Number of records deleted
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -318,7 +311,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
Number of records deleted Number of records deleted
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
sql = f"DELETE FROM {table_name}" sql = f"DELETE FROM {table_name}"
try: try:
@@ -340,7 +333,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
True if material exists, False otherwise True if material exists, False otherwise
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -359,7 +352,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
result = db.execute_query(sql, (material_name,)) result = db.execute_query(sql, (material_name,))
return result[0]['count'] > 0 if result else False return result[0]["count"] > 0 if result else False
def count_by_manager(self, manager_name: str) -> int: def count_by_manager(self, manager_name: str) -> int:
""" """
@@ -371,7 +364,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
Number of materials for the manager Number of materials for the manager
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -390,7 +383,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
result = db.execute_query(sql, (manager_name,)) result = db.execute_query(sql, (manager_name,))
return result[0]['count'] if result else 0 return result[0]["count"] if result else 0
def get_statistics(self) -> Dict[str, Any]: def get_statistics(self) -> Dict[str, Any]:
""" """
@@ -400,7 +393,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Dictionary with statistics including total materials, Dictionary with statistics including total materials,
unique managers, and materials per manager unique managers, and materials per manager
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -442,8 +435,8 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
# Get materials per manager # Get materials per manager
manager_results = db.execute_query(manager_sql) manager_results = db.execute_query(manager_sql)
stats['materials_per_manager'] = [ stats["materials_per_manager"] = [
{r['ManagerName']: r['count']} for r in manager_results {r["ManagerName"]: r["count"]} for r in manager_results
] ]
return stats return stats
@@ -458,7 +451,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
Returns: Returns:
List of matching materials List of matching materials
""" """
table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -478,4 +471,4 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO):
""" """
with get_connection() as db: with get_connection() as db:
return db.execute_query(sql, (f'%{keyword}%',)) return db.execute_query(sql, (f"%{keyword}%",))

View File

@@ -40,7 +40,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
# MySQL 使用 INSERT ... ON DUPLICATE KEY UPDATE # MySQL 使用 INSERT ... ON DUPLICATE KEY UPDATE
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -57,7 +57,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
""" """
else: else:
# SQL Server 使用 MERGE # SQL Server 使用 MERGE
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
sql = f""" sql = f"""
MERGE {table_name} AS target MERGE {table_name} AS target
USING (SELECT {self._get_placeholder()} AS MaterialCode, {self._get_placeholder()} AS ManagerName) AS source USING (SELECT {self._get_placeholder()} AS MaterialCode, {self._get_placeholder()} AS ManagerName) AS source
@@ -69,7 +69,13 @@ class MaterialsToBeDeletedDAO(BaseDAO):
VALUES (source.MaterialCode, source.ManagerName); VALUES (source.MaterialCode, source.ManagerName);
""" """
db.execute_update(sql, (material_code.strip(), manager_name.strip() if manager_name else None)) db.execute_update(
sql,
(
material_code.strip(),
manager_name.strip() if manager_name else None,
),
)
return True return True
except Exception as e: except Exception as e:
print(f"Error upserting material: {e}") print(f"Error upserting material: {e}")
@@ -86,24 +92,26 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Dictionary with statistics: {'total': int, 'success': int, 'failed': int} Dictionary with statistics: {'total': int, 'success': int, 'failed': int}
""" """
if not materials: if not materials:
return {'total': 0, 'success': 0, 'failed': 0} return {"total": 0, "success": 0, "failed": 0}
stats = {'total': len(materials), 'success': 0, 'failed': 0} stats = {"total": len(materials), "success": 0, "failed": 0}
try: try:
with get_connection() as db: with get_connection() as db:
for material in materials: for material in materials:
material_code = material.get('material_code', '').strip() material_code = material.get("material_code", "").strip()
manager_name = material.get('manager_name', '') manager_name = material.get("manager_name", "")
if not material_code: if not material_code:
stats['failed'] += 1 stats["failed"] += 1
continue continue
try: try:
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
# MySQL 使用 INSERT ... ON DUPLICATE KEY UPDATE # MySQL 使用 INSERT ... ON DUPLICATE KEY UPDATE
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql(
"[dbo].[MaterialsToBeDeleted]"
)
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -120,7 +128,9 @@ class MaterialsToBeDeletedDAO(BaseDAO):
""" """
else: else:
# SQL Server 使用 MERGE # SQL Server 使用 MERGE
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql(
"[dbo].[MaterialsToBeDeleted]"
)
sql = f""" sql = f"""
MERGE {table_name} AS target MERGE {table_name} AS target
USING (SELECT {self._get_placeholder()} AS MaterialCode, {self._get_placeholder()} AS ManagerName) AS source USING (SELECT {self._get_placeholder()} AS MaterialCode, {self._get_placeholder()} AS ManagerName) AS source
@@ -132,15 +142,21 @@ class MaterialsToBeDeletedDAO(BaseDAO):
VALUES (source.MaterialCode, source.ManagerName); VALUES (source.MaterialCode, source.ManagerName);
""" """
db.execute_update(sql, (material_code, manager_name.strip() if manager_name else None)) db.execute_update(
stats['success'] += 1 sql,
(
material_code,
manager_name.strip() if manager_name else None,
),
)
stats["success"] += 1
except Exception as e: except Exception as e:
print(f"Error upserting material {material_code}: {e}") print(f"Error upserting material {material_code}: {e}")
stats['failed'] += 1 stats["failed"] += 1
except Exception as e: except Exception as e:
print(f"Error in batch upsert: {e}") print(f"Error in batch upsert: {e}")
stats['failed'] = stats['total'] - stats['success'] stats["failed"] = stats["total"] - stats["success"]
return stats return stats
@@ -153,7 +169,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
Set of material codes Set of material codes
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -172,7 +188,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
try: try:
with get_connection() as db: with get_connection() as db:
results = db.execute_query(sql) results = db.execute_query(sql)
return {r['MaterialCode'] for r in results if r.get('MaterialCode')} return {r["MaterialCode"] for r in results if r.get("MaterialCode")}
except Exception as e: except Exception as e:
print(f"Error getting material codes: {e}") print(f"Error getting material codes: {e}")
return set() return set()
@@ -184,7 +200,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
List of all material records with all fields List of all material records with all fields
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -215,7 +231,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
List of materials for the specified manager List of materials for the specified manager
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -244,7 +260,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
List of unique manager names List of unique manager names
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -264,7 +280,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
results = db.execute_query(sql) results = db.execute_query(sql)
return [r['ManagerName'] for r in results if r.get('ManagerName')] return [r["ManagerName"] for r in results if r.get("ManagerName")]
def get_records_by_manager(self, manager_name: str) -> List[Dict[str, Any]]: def get_records_by_manager(self, manager_name: str) -> List[Dict[str, Any]]:
""" """
@@ -280,7 +296,9 @@ class MaterialsToBeDeletedDAO(BaseDAO):
""" """
return self.get_materials_by_manager(manager_name) return self.get_materials_by_manager(manager_name)
def get_record_by_material_code(self, material_code: str) -> Optional[Dict[str, Any]]: def get_record_by_material_code(
self, material_code: str
) -> Optional[Dict[str, Any]]:
""" """
Get a specific record by material code. Get a specific record by material code.
@@ -290,7 +308,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
Dictionary representing the record, or None if not found Dictionary representing the record, or None if not found
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -323,7 +341,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
True if successful, False otherwise True if successful, False otherwise
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -356,7 +374,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
Number of records deleted Number of records deleted
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -385,7 +403,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
Number of records deleted Number of records deleted
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
sql = f"DELETE FROM {table_name}" sql = f"DELETE FROM {table_name}"
try: try:
@@ -412,16 +430,18 @@ class MaterialsToBeDeletedDAO(BaseDAO):
total_deleted = 0 total_deleted = 0
for i in range(0, len(material_codes), batch_size): for i in range(0, len(material_codes), batch_size):
batch = material_codes[i:i + batch_size] batch = material_codes[i : i + batch_size]
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch]) placeholders = ",".join([placeholder for _ in batch])
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
sql = f"DELETE FROM {table_name} WHERE MaterialCode IN ({placeholders})" sql = f"DELETE FROM {table_name} WHERE MaterialCode IN ({placeholders})"
else: else:
sql = f"DELETE FROM {table_name} WHERE [MaterialCode] IN ({placeholders})" sql = (
f"DELETE FROM {table_name} WHERE [MaterialCode] IN ({placeholders})"
)
try: try:
with get_connection() as db: with get_connection() as db:
@@ -444,7 +464,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
True if material exists, False otherwise True if material exists, False otherwise
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -463,7 +483,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
result = db.execute_query(sql, (material_code.strip(),)) result = db.execute_query(sql, (material_code.strip(),))
return result[0]['count'] > 0 if result else False return result[0]["count"] > 0 if result else False
def count_all(self) -> int: def count_all(self) -> int:
""" """
@@ -472,12 +492,12 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
Total number of records Total number of records
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
sql = f"SELECT COUNT(*) as count FROM {table_name}" sql = f"SELECT COUNT(*) as count FROM {table_name}"
with get_connection() as db: with get_connection() as db:
result = db.execute_query(sql) result = db.execute_query(sql)
return result[0]['count'] if result else 0 return result[0]["count"] if result else 0
def count_by_manager(self, manager_name: str) -> int: def count_by_manager(self, manager_name: str) -> int:
""" """
@@ -489,7 +509,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Returns: Returns:
Number of materials for the manager Number of materials for the manager
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
@@ -508,7 +528,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
with get_connection() as db: with get_connection() as db:
result = db.execute_query(sql, (manager_name,)) result = db.execute_query(sql, (manager_name,))
return result[0]['count'] if result else 0 return result[0]["count"] if result else 0
def get_statistics(self) -> Dict[str, Any]: def get_statistics(self) -> Dict[str, Any]:
""" """
@@ -518,7 +538,7 @@ class MaterialsToBeDeletedDAO(BaseDAO):
Dictionary with statistics including total materials, Dictionary with statistics including total materials,
unique managers, and materials per manager unique managers, and materials per manager
""" """
table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]")
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -560,8 +580,8 @@ class MaterialsToBeDeletedDAO(BaseDAO):
# Get materials per manager # Get materials per manager
manager_results = db.execute_query(manager_sql) manager_results = db.execute_query(manager_sql)
stats['materials_per_manager'] = [ stats["materials_per_manager"] = [
{r['ManagerName']: r['count']} for r in manager_results {r["ManagerName"]: r["count"]} for r in manager_results
] ]
return stats return stats

View File

@@ -72,7 +72,9 @@ def get_materials_to_delete_by_managers(
""" """
with get_connection() as conn: with get_connection() as conn:
results = conn.execute_query(query) results = conn.execute_query(query)
material_codes = [row["MaterialCode"] for row in results if row["MaterialCode"]] material_codes = [
row["MaterialCode"] for row in results if row["MaterialCode"]
]
return material_codes return material_codes
else: else:
# 使用 IN 子句查询多个负责人 # 使用 IN 子句查询多个负责人
@@ -85,7 +87,9 @@ def get_materials_to_delete_by_managers(
""" """
with get_connection() as conn: with get_connection() as conn:
results = conn.execute_query(query, tuple(manager_names)) results = conn.execute_query(query, tuple(manager_names))
material_codes = [row["MaterialCode"] for row in results if row["MaterialCode"]] material_codes = [
row["MaterialCode"] for row in results if row["MaterialCode"]
]
return material_codes return material_codes

View File

@@ -40,13 +40,13 @@ class MySQLConnection(BaseDatabaseConnection):
try: try:
self.connection = mysql.connector.connect( self.connection = mysql.connector.connect(
host=self.config.get('host', 'localhost'), host=self.config.get("host", "localhost"),
port=self.config.get('port', 3306), port=self.config.get("port", 3306),
database=self.config['database'], database=self.config["database"],
user=self.config['username'], user=self.config["username"],
password=self.config['password'], password=self.config["password"],
charset=self.config.get('charset', 'utf8mb4'), charset=self.config.get("charset", "utf8mb4"),
autocommit=False autocommit=False,
) )
print( print(
f"成功连接到 MySQL 数据库: {self.config.get('host', 'localhost')}" f"成功连接到 MySQL 数据库: {self.config.get('host', 'localhost')}"
@@ -153,7 +153,7 @@ class MySQLConnection(BaseDatabaseConnection):
Returns: Returns:
str: SQL query with MySQL-compatible placeholders str: SQL query with MySQL-compatible placeholders
""" """
return sql.replace('?', '%s') return sql.replace("?", "%s")
def _convert_table_names(self, sql: str) -> str: def _convert_table_names(self, sql: str) -> str:
""" """
@@ -171,10 +171,10 @@ class MySQLConnection(BaseDatabaseConnection):
import re import re
# Convert [dbo].[TableName] to dbo_TableName # Convert [dbo].[TableName] to dbo_TableName
sql = re.sub(r'\[dbo\]\.\[([^\]]+)\]', r'dbo_\1', sql) sql = re.sub(r"\[dbo\]\.\[([^\]]+)\]", r"dbo_\1", sql)
# Remove square brackets from column names (e.g., [Column] -> Column) # Remove square brackets from column names (e.g., [Column] -> Column)
sql = re.sub(r'\[([^\]]+)\]', r'\1', sql) sql = re.sub(r"\[([^\]]+)\]", r"\1", sql)
return sql return sql

View File

@@ -32,12 +32,14 @@ class ProductionContractDataDAO(BaseDAO):
all_results = [] all_results = []
for i in range(0, len(总排号_list), batch_size): for i in range(0, len(总排号_list), batch_size):
batch = 总排号_list[i:i + batch_size] batch = 总排号_list[i : i + batch_size]
placeholder = self._get_placeholder() placeholder = self._get_placeholder()
placeholders = ','.join([placeholder for _ in batch]) placeholders = ",".join([placeholder for _ in batch])
# 根据数据库类型选择表名 # 根据数据库类型选择表名
table_name = self._convert_sql('[productionContractData].[26年压力表合同数据]') table_name = self._convert_sql(
"[productionContractData].[26年压力表合同数据]"
)
# 根据数据库类型选择列名格式 # 根据数据库类型选择列名格式
if self._db_type == DatabaseType.MYSQL: if self._db_type == DatabaseType.MYSQL:
@@ -73,9 +75,9 @@ class ProductionContractDataDAO(BaseDAO):
""" """
results = self.query_by_总排号(总排号_list) results = self.query_by_总排号(总排号_list)
# Extract unique 生产订单号 values, excluding None/null values # Extract unique 生产订单号 values, excluding None/null values
source_numbers = list(set( source_numbers = list(
[r['生产订单号'] for r in results if r.get('生产订单号')] set([r["生产订单号"] for r in results if r.get("生产订单号")])
)) )
return source_numbers return source_numbers
def get_生产订单号_map(self, 总排号_list: List[str]) -> Dict[str, str]: def get_生产订单号_map(self, 总排号_list: List[str]) -> Dict[str, str]:
@@ -90,7 +92,7 @@ class ProductionContractDataDAO(BaseDAO):
""" """
results = self.query_by_总排号(总排号_list) results = self.query_by_总排号(总排号_list)
return { return {
r['总排号']: r['生产订单号'] r["总排号"]: r["生产订单号"]
for r in results for r in results
if r.get('总排号') and r.get('生产订单号') if r.get("总排号") and r.get("生产订单号")
} }

View File

@@ -95,7 +95,9 @@ def _query_order_numbers_from_db(production_ids, db_type):
results = conn.execute_query(query, tuple(batch)) results = conn.execute_query(query, tuple(batch))
# 提取生产订单号并去除空值 # 提取生产订单号并去除空值
batch_numbers = [row["生产订单号"] for row in results if row.get("生产订单号")] 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
@@ -118,7 +120,7 @@ def query_production_order_numbers(inputs):
return [] return []
production_ids = [] # 需要查询数据库的 production_ids = [] # 需要查询数据库的
order_numbers = [] # 直接使用的 order_numbers = [] # 直接使用的
for item in inputs: for item in inputs:
input_type = identify_input_type(item) input_type = identify_input_type(item)

View File

@@ -38,7 +38,7 @@ class SQLServerConnection(BaseDatabaseConnection):
return self.connection return self.connection
# 构建连接字符串 # 构建连接字符串
driver = self.config.get('driver', 'ODBC Driver 18 for SQL Server') driver = self.config.get("driver", "ODBC Driver 18 for SQL Server")
conn_str = ( conn_str = (
f"DRIVER={{{driver}}};" f"DRIVER={{{driver}}};"
f"SERVER={self.config['server']};" f"SERVER={self.config['server']};"

View File

@@ -12,7 +12,7 @@ class TableNameConverter:
"""表名转换工具类""" """表名转换工具类"""
# 匹配 SQL Server 表名格式:[schema].[tablename] 或 [schema].[table name] # 匹配 SQL Server 表名格式:[schema].[tablename] 或 [schema].[table name]
SQLSERVER_PATTERN = re.compile(r'\[([^\]]+)\]\.\[([^\]]+)\]') SQLSERVER_PATTERN = re.compile(r"\[([^\]]+)\]\.\[([^\]]+)\]")
@staticmethod @staticmethod
def to_mysql(table_name: str) -> str: def to_mysql(table_name: str) -> str:
@@ -66,7 +66,7 @@ class TableNameConverter:
'[productionContractData].[26年压力表合同数据]' '[productionContractData].[26年压力表合同数据]'
""" """
# 分割第一个下划线 # 分割第一个下划线
parts = table_name.split('_', 1) parts = table_name.split("_", 1)
if len(parts) == 2: if len(parts) == 2:
schema = parts[0] schema = parts[0]
table = parts[1] table = parts[1]
@@ -92,21 +92,24 @@ class TableNameConverter:
>>> TableNameConverter.convert_sql(sql, 'mysql') >>> TableNameConverter.convert_sql(sql, 'mysql')
'SELECT * FROM dbo_BIPUsers WHERE ID = ?' 'SELECT * FROM dbo_BIPUsers WHERE ID = ?'
""" """
if db_type == 'mysql': if db_type == "mysql":
# SQL Server → MySQL # SQL Server → MySQL
def replace_to_mysql(match): def replace_to_mysql(match):
schema = match.group(1) schema = match.group(1)
table = match.group(2) table = match.group(2)
return f"{schema}_{table}" return f"{schema}_{table}"
result = TableNameConverter.SQLSERVER_PATTERN.sub(replace_to_mysql, sql) result = TableNameConverter.SQLSERVER_PATTERN.sub(replace_to_mysql, sql)
return result return result
elif db_type == 'sqlserver': elif db_type == "sqlserver":
# MySQL → SQL Server # MySQL → SQL Server
# 首先查找可能的 MySQL 格式表名schema_table 格式) # 首先查找可能的 MySQL 格式表名schema_table 格式)
# 这是一个简化版本,可能无法处理所有边缘情况 # 这是一个简化版本,可能无法处理所有边缘情况
result = sql result = sql
# 查找单词字符_单词字符 的模式(可能是表名) # 查找单词字符_单词字符 的模式(可能是表名)
mysql_pattern = re.compile(r'\b([a-zA-Z_][a-zA-Z0-9_]*)_([a-zA-Z0-9_\u4e00-\u9fff]+)\b') mysql_pattern = re.compile(
r"\b([a-zA-Z_][a-zA-Z0-9_]*)_([a-zA-Z0-9_\u4e00-\u9fff]+)\b"
)
matches = mysql_pattern.findall(result) matches = mysql_pattern.findall(result)
for schema, table in set(matches): for schema, table in set(matches):
mysql_name = f"{schema}_{table}" mysql_name = f"{schema}_{table}"
@@ -133,7 +136,7 @@ class TableNameConverter:
tables.append(f"{schema}_{table}") tables.append(f"{schema}_{table}")
# 查找可能的 MySQL 格式 # 查找可能的 MySQL 格式
mysql_pattern = re.compile(r'\b[a-zA-Z_][a-zA-Z0-9_]*_[a-zA-Z0-9_]+\b') mysql_pattern = re.compile(r"\b[a-zA-Z_][a-zA-Z0-9_]*_[a-zA-Z0-9_]+\b")
mysql_matches = mysql_pattern.findall(sql) mysql_matches = mysql_pattern.findall(sql)
tables.extend(mysql_matches) tables.extend(mysql_matches)

View File

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

View File

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

View File

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

View File

@@ -45,11 +45,11 @@ HEADING_STORE_DELAY_MS = 100
# 日志级别对应的颜色 # 日志级别对应的颜色
LOG_COLORS = { LOG_COLORS = {
"INFO": "#000000", # 黑色 "INFO": "#000000", # 黑色
"SUCCESS": "#008000", # 绿色 "SUCCESS": "#008000", # 绿色
"WARNING": "#FF8C00", # 橙色 "WARNING": "#FF8C00", # 橙色
"ERROR": "#FF0000", # 红色 "ERROR": "#FF0000", # 红色
"DEBUG": "#808080", # 灰色 "DEBUG": "#808080", # 灰色
} }
# ============================================================================ # ============================================================================

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@
将现有的 JSON 配置文件迁移到 .env 环境变量文件 将现有的 JSON 配置文件迁移到 .env 环境变量文件
""" """
import os import os
import sys import sys
import json import json
@@ -21,7 +22,7 @@ from config.schema import AppConfig
def migrate_json_to_env( def migrate_json_to_env(
json_file: str = "config/user_settings.json", json_file: str = "config/user_settings.json",
env_file: str = ".env", env_file: str = ".env",
backup: bool = True backup: bool = True,
) -> bool: ) -> bool:
""" """
迁移 JSON 配置到 .env 文件 迁移 JSON 配置到 .env 文件
@@ -46,7 +47,7 @@ def migrate_json_to_env(
# 检查 .env 文件是否已存在 # 检查 .env 文件是否已存在
if env_path.exists(): if env_path.exists():
response = input(f"⚠️ .env 文件已存在: {env_path}\n是否覆盖? (y/N): ") response = input(f"⚠️ .env 文件已存在: {env_path}\n是否覆盖? (y/N): ")
if response.lower() != 'y': if response.lower() != "y":
print("❌ 迁移已取消") print("❌ 迁移已取消")
return False return False
@@ -63,6 +64,7 @@ def migrate_json_to_env(
# 使用 ConfigLoader 将字典转换为配置对象 # 使用 ConfigLoader 将字典转换为配置对象
from config.loader import ConfigLoader from config.loader import ConfigLoader
config = ConfigLoader._dict_to_config(json_data) config = ConfigLoader._dict_to_config(json_data)
# 保存到 .env 文件 # 保存到 .env 文件
@@ -95,13 +97,13 @@ def migrate_json_to_env(
except Exception as e: except Exception as e:
print(f"❌ 迁移失败: {e}") print(f"❌ 迁移失败: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return False return False
def create_env_from_example( def create_env_from_example(
example_file: str = ".env.example", example_file: str = ".env.example", env_file: str = ".env"
env_file: str = ".env"
) -> bool: ) -> bool:
""" """
从 .env.example 创建 .env 文件 从 .env.example 创建 .env 文件
@@ -122,7 +124,7 @@ def create_env_from_example(
if env_path.exists(): if env_path.exists():
response = input(f"⚠️ .env 文件已存在: {env_path}\n是否覆盖? (y/N): ") response = input(f"⚠️ .env 文件已存在: {env_path}\n是否覆盖? (y/N): ")
if response.lower() != 'y': if response.lower() != "y":
print("❌ 操作已取消") print("❌ 操作已取消")
return False return False
@@ -160,7 +162,9 @@ def main():
elif command == "migrate": elif command == "migrate":
# 从 JSON 迁移 # 从 JSON 迁移
print("\n📋 模式: 从 JSON 配置迁移") print("\n📋 模式: 从 JSON 配置迁移")
json_file = sys.argv[2] if len(sys.argv) > 2 else "config/user_settings.json" json_file = (
sys.argv[2] if len(sys.argv) > 2 else "config/user_settings.json"
)
env_file = sys.argv[3] if len(sys.argv) > 3 else ".env" env_file = sys.argv[3] if len(sys.argv) > 3 else ".env"
migrate_json_to_env(json_file, env_file) migrate_json_to_env(json_file, env_file)
return return
@@ -192,7 +196,9 @@ def main():
choice = input("\n请输入选项 (1-3): ").strip() choice = input("\n请输入选项 (1-3): ").strip()
if choice == "1": if choice == "1":
json_file = input("JSON 配置文件路径 (默认: config/user_settings.json): ").strip() json_file = input(
"JSON 配置文件路径 (默认: config/user_settings.json): "
).strip()
if not json_file: if not json_file:
json_file = "config/user_settings.json" json_file = "config/user_settings.json"
@@ -201,7 +207,7 @@ def main():
env_file = ".env" env_file = ".env"
backup_choice = input("是否备份原 JSON 文件? (Y/n): ").strip().lower() backup_choice = input("是否备份原 JSON 文件? (Y/n): ").strip().lower()
backup = backup_choice != 'n' backup = backup_choice != "n"
migrate_json_to_env(json_file, env_file, backup) migrate_json_to_env(json_file, env_file, backup)

View File

@@ -65,16 +65,16 @@ class DiscreteMaterialPlanCleaner:
# 统计信息 # 统计信息
self.stats = { self.stats = {
'total_orders': 0, "total_orders": 0,
'processed_orders': 0, "processed_orders": 0,
'total_materials': 0, # 总物料数 "total_materials": 0, # 总物料数
'processed_materials': 0, # 已处理物料数 "processed_materials": 0, # 已处理物料数
'deleted_materials': [], # [{order_id, material_code, material_name}] "deleted_materials": [], # [{order_id, material_code, material_name}]
'skipped_materials': [], # [{order_id, material_code, material_name, reason}] "skipped_materials": [], # [{order_id, material_code, material_name, reason}]
'unmatched_materials': [], # [{order_id, material_code, material_name}] 不在删除列表的物料 "unmatched_materials": [], # [{order_id, material_code, material_name}] 不在删除列表的物料
'errors': [], # [{order_id, error_message}] "errors": [], # [{order_id, error_message}]
'start_time': None, "start_time": None,
'end_time': None, "end_time": None,
} }
def _log(self, message, level="info"): def _log(self, message, level="info"):
@@ -100,7 +100,7 @@ class DiscreteMaterialPlanCleaner:
total_materials: int, total_materials: int,
order_id: str, order_id: str,
material_name: str, material_name: str,
action: str action: str,
): ):
"""报告物料处理进度 """报告物料处理进度
@@ -166,7 +166,9 @@ class DiscreteMaterialPlanCleaner:
) )
return order_ids return order_ids
def process_order(self, inner_frame, order_id, order_index, page1, total_orders: int = 1): def process_order(
self, inner_frame, order_id, order_index, page1, total_orders: int = 1
):
"""清理单个订单的数据 """清理单个订单的数据
Args: Args:
@@ -225,7 +227,7 @@ class DiscreteMaterialPlanCleaner:
if detail_status == "审批通过": if detail_status == "审批通过":
if detail_count > 0: if detail_count > 0:
# 更新总物料数统计 # 更新总物料数统计
self.stats['total_materials'] += detail_count self.stats["total_materials"] += detail_count
# --- 点击修改并等待状态切换 (保留原逻辑) --- # --- 点击修改并等待状态切换 (保留原逻辑) ---
detail_inner_frame.get_by_role("button", name="修改").click() detail_inner_frame.get_by_role("button", name="修改").click()
@@ -252,7 +254,7 @@ class DiscreteMaterialPlanCleaner:
# page2.pause() # 调试用,正式运行时可删除 # page2.pause() # 调试用,正式运行时可删除
while True: while True:
material_idx += 1 material_idx += 1
self.stats['processed_materials'] += 1 self.stats["processed_materials"] += 1
# 稳定性检查:等待行号更新 # 稳定性检查:等待行号更新
current_row = self._get_input_value(child_form, r"^行号$") current_row = self._get_input_value(child_form, r"^行号$")
@@ -262,25 +264,37 @@ class DiscreteMaterialPlanCleaner:
material_code = self._get_input_value(child_form, r"^材料编码") material_code = self._get_input_value(child_form, r"^材料编码")
material_name = self._get_input_value(child_form, r"^材料名称") material_name = self._get_input_value(child_form, r"^材料名称")
pending_qty = self._get_input_value(child_form, r"^累计待发数量$") pending_qty = self._get_input_value(
child_form, r"^累计待发数量$"
)
# 报告物料进度 # 报告物料进度
self._report_material_progress( self._report_material_progress(
order_index, total_orders, order_index,
material_idx, detail_count, total_orders,
order_id, material_name, "检查" material_idx,
detail_count,
order_id,
material_name,
"检查",
) )
if material_code in self.to_delete_set: if material_code in self.to_delete_set:
self._log(f"发现匹配物料: {material_name} ({material_code})") self._log(
f"发现匹配物料: {material_name} ({material_code})"
)
if (not pending_qty) and not ( if (not pending_qty) and not (
row_num_int >= 7000 and row_num_int < 8000 row_num_int >= 7000 and row_num_int < 8000
): ):
# 报告删除进度 # 报告删除进度
self._report_material_progress( self._report_material_progress(
order_index, total_orders, order_index,
material_idx, detail_count, total_orders,
order_id, material_name, "删除" material_idx,
detail_count,
order_id,
material_name,
"删除",
) )
# 记录删除前的行号 # 记录删除前的行号
old_row_number = current_row old_row_number = current_row
@@ -308,75 +322,98 @@ class DiscreteMaterialPlanCleaner:
time.sleep(0.2) time.sleep(0.2)
else: else:
self._log( self._log(
f"⚠️ 等待删除完成超时({max_wait_time}秒)", "warn" f"⚠️ 等待删除完成超时({max_wait_time}秒)",
"warn",
) )
# 记录删除统计 # 记录删除统计
if delete_success: if delete_success:
self.stats['deleted_materials'].append({ self.stats["deleted_materials"].append(
'order_id': order_id, {
'material_code': material_code, "order_id": order_id,
'material_name': material_name "material_code": material_code,
}) "material_name": material_name,
}
)
continue continue
elif row_num_int >= 7000 and row_num_int < 8000: elif row_num_int >= 7000 and row_num_int < 8000:
reason = f"行号 {row_num_int} 在 7000-8000 范围内" reason = f"行号 {row_num_int} 在 7000-8000 范围内"
self._report_material_progress( self._report_material_progress(
order_index, total_orders, order_index,
material_idx, detail_count, total_orders,
order_id, material_name, "跳过" material_idx,
detail_count,
order_id,
material_name,
"跳过",
) )
self._log(f"⚠️ {reason},跳过删除", "warn") self._log(f"⚠️ {reason},跳过删除", "warn")
self.stats['skipped_materials'].append({ self.stats["skipped_materials"].append(
'order_id': order_id, {
'material_code': material_code, "order_id": order_id,
'material_name': material_name, "material_code": material_code,
'reason': reason "material_name": material_name,
}) "reason": reason,
}
)
elif pending_qty: elif pending_qty:
reason = f"待发数量为 {pending_qty}" reason = f"待发数量为 {pending_qty}"
self._report_material_progress( self._report_material_progress(
order_index, total_orders, order_index,
material_idx, detail_count, total_orders,
order_id, material_name, "跳过" material_idx,
detail_count,
order_id,
material_name,
"跳过",
) )
self._log(f"⚠️ {reason},跳过删除", "warn") self._log(f"⚠️ {reason},跳过删除", "warn")
self.stats['skipped_materials'].append({ self.stats["skipped_materials"].append(
'order_id': order_id, {
'material_code': material_code, "order_id": order_id,
'material_name': material_name, "material_code": material_code,
'reason': reason "material_name": material_name,
}) "reason": reason,
}
)
else: else:
reason = "不满足删除条件" reason = "不满足删除条件"
self._report_material_progress( self._report_material_progress(
order_index, total_orders, order_index,
material_idx, detail_count, total_orders,
order_id, material_name, "跳过" material_idx,
detail_count,
order_id,
material_name,
"跳过",
) )
self._log( self._log(
f"⚠️ {reason},跳过物料 {material_name} ({material_code})", f"⚠️ {reason},跳过物料 {material_name} ({material_code})",
"warn", "warn",
) )
self.stats['skipped_materials'].append({ self.stats["skipped_materials"].append(
'order_id': order_id, {
'material_code': material_code, "order_id": order_id,
'material_name': material_name, "material_code": material_code,
'reason': reason "material_name": material_name,
}) "reason": reason,
}
)
else: else:
self._log( self._log(
f" 物料 {material_name} ({material_code}) 不在删除列表中,不做处理" f" 物料 {material_name} ({material_code}) 不在删除列表中,不做处理"
) )
# 记录到 unmatched_materials # 记录到 unmatched_materials
self.stats['unmatched_materials'].append({ self.stats["unmatched_materials"].append(
'order_id': order_id, {
'material_code': material_code, "order_id": order_id,
'material_name': material_name "material_code": material_code,
}) "material_name": material_name,
}
)
if self._is_button_enabled(next_btn): if self._is_button_enabled(next_btn):
last_row_number = current_row last_row_number = current_row
next_btn.click() next_btn.click()
@@ -417,7 +454,7 @@ class DiscreteMaterialPlanCleaner:
def clean(self, production_id_file): def clean(self, production_id_file):
"""执行完整清理流程""" """执行完整清理流程"""
# 初始化统计 # 初始化统计
self.stats['start_time'] = datetime.now() self.stats["start_time"] = datetime.now()
# 0. 预加载数据库数据 # 0. 预加载数据库数据
self.preload_data() self.preload_data()
@@ -450,20 +487,21 @@ class DiscreteMaterialPlanCleaner:
order_ids = self.get_production_order_numbers(production_id_file) order_ids = self.get_production_order_numbers(production_id_file)
# 设置总订单数 # 设置总订单数
self.stats['total_orders'] = len(order_ids) self.stats["total_orders"] = len(order_ids)
# 遍历处理 # 遍历处理
for index, order_id in enumerate(order_ids): for index, order_id in enumerate(order_ids):
self._log(f"进度: [{index+1}/{len(order_ids)}] 处理单号: {order_id}") self._log(f"进度: [{index+1}/{len(order_ids)}] 处理单号: {order_id}")
try: try:
self.process_order(inner_frame, order_id, index, page1, len(order_ids)) self.process_order(
self.stats['processed_orders'] += 1 inner_frame, order_id, index, page1, len(order_ids)
)
self.stats["processed_orders"] += 1
except Exception as e: except Exception as e:
self._log(f"处理单号 {order_id} 时发生异常: {e}", "error") self._log(f"处理单号 {order_id} 时发生异常: {e}", "error")
self.stats['errors'].append({ self.stats["errors"].append(
'order_id': order_id, {"order_id": order_id, "error_message": str(e)}
'error_message': str(e) )
})
continue # 单个失败不影响整体执行 continue # 单个失败不影响整体执行
# 登出清理 # 登出清理
@@ -473,7 +511,7 @@ class DiscreteMaterialPlanCleaner:
self._log("=" * 30 + " 任务全部完成 " + "=" * 30) self._log("=" * 30 + " 任务全部完成 " + "=" * 30)
# 记录结束时间 # 记录结束时间
self.stats['end_time'] = datetime.now() self.stats["end_time"] = datetime.now()
def generate_report(self) -> str: def generate_report(self) -> str:
"""生成 Markdown 格式的执行报告 """生成 Markdown 格式的执行报告
@@ -490,62 +528,78 @@ class DiscreteMaterialPlanCleaner:
# 概述 # 概述
report_lines.append("## 概述") report_lines.append("## 概述")
report_lines.append("") report_lines.append("")
start_time = self.stats.get('start_time') start_time = self.stats.get("start_time")
end_time = self.stats.get('end_time') end_time = self.stats.get("end_time")
duration = None duration = None
if start_time and end_time: if start_time and end_time:
duration = end_time - start_time duration = end_time - start_time
report_lines.append(f"- 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") report_lines.append(
f"- 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}"
)
report_lines.append(f"- 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") report_lines.append(f"- 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
report_lines.append(f"- 执行时长: {duration}") report_lines.append(f"- 执行时长: {duration}")
report_lines.append(f"- 处理订单: {self.stats['processed_orders']}/{self.stats['total_orders']}") report_lines.append(
report_lines.append(f"- 处理物料: {self.stats['processed_materials']}/{self.stats['total_materials']} ") f"- 处理订单: {self.stats['processed_orders']}/{self.stats['total_orders']} "
)
report_lines.append(
f"- 处理物料: {self.stats['processed_materials']}/{self.stats['total_materials']}"
)
report_lines.append(f"- 删除物料: {len(self.stats['deleted_materials'])}") report_lines.append(f"- 删除物料: {len(self.stats['deleted_materials'])}")
report_lines.append(f"- 跳过物料: {len(self.stats['skipped_materials'])}") report_lines.append(f"- 跳过物料: {len(self.stats['skipped_materials'])}")
report_lines.append(f"- 未处理物料: {len(self.stats['unmatched_materials'])}") report_lines.append(
f"- 未处理物料: {len(self.stats['unmatched_materials'])}"
)
report_lines.append(f"- 错误数量: {len(self.stats['errors'])}") report_lines.append(f"- 错误数量: {len(self.stats['errors'])}")
report_lines.append(f"- 执行模式: {'预览模式 (dryrun)' if self.dryrun else '正常执行'}") report_lines.append(
f"- 执行模式: {'预览模式 (dryrun)' if self.dryrun else '正常执行'}"
)
if self.manager_names: if self.manager_names:
report_lines.append(f"- 负责人: {', '.join(self.manager_names)}") report_lines.append(f"- 负责人: {', '.join(self.manager_names)}")
report_lines.append("") report_lines.append("")
# 删除明细 # 删除明细
if self.stats['deleted_materials']: if self.stats["deleted_materials"]:
report_lines.append("## 删除明细") report_lines.append("## 删除明细")
report_lines.append("") report_lines.append("")
report_lines.append("| 订单号 | 物料编码 | 物料名称 |") report_lines.append("| 订单号 | 物料编码 | 物料名称 |")
report_lines.append("|--------|----------|----------|") report_lines.append("|--------|----------|----------|")
for item in self.stats['deleted_materials']: for item in self.stats["deleted_materials"]:
report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |") report_lines.append(
f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |"
)
report_lines.append("") report_lines.append("")
# 跳过明细 # 跳过明细
if self.stats['skipped_materials']: if self.stats["skipped_materials"]:
report_lines.append("## 跳过明细") report_lines.append("## 跳过明细")
report_lines.append("") report_lines.append("")
report_lines.append("| 订单号 | 物料编码 | 物料名称 | 跳过原因 |") report_lines.append("| 订单号 | 物料编码 | 物料名称 | 跳过原因 |")
report_lines.append("|--------|----------|----------|----------|") report_lines.append("|--------|----------|----------|----------|")
for item in self.stats['skipped_materials']: for item in self.stats["skipped_materials"]:
report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} | {item['reason']} |") report_lines.append(
f"| {item['order_id']} | {item['material_code']} | {item['material_name']} | {item['reason']} |"
)
report_lines.append("") report_lines.append("")
# 未处理物料明细 # 未处理物料明细
if self.stats['unmatched_materials']: if self.stats["unmatched_materials"]:
report_lines.append("## 未处理物料") report_lines.append("## 未处理物料")
report_lines.append("") report_lines.append("")
report_lines.append("| 订单号 | 物料编码 | 物料名称 |") report_lines.append("| 订单号 | 物料编码 | 物料名称 |")
report_lines.append("|--------|----------|----------|") report_lines.append("|--------|----------|----------|")
for item in self.stats['unmatched_materials']: for item in self.stats["unmatched_materials"]:
report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |") report_lines.append(
f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |"
)
report_lines.append("") report_lines.append("")
# 错误明细 # 错误明细
if self.stats['errors']: if self.stats["errors"]:
report_lines.append("## 错误明细") report_lines.append("## 错误明细")
report_lines.append("") report_lines.append("")
report_lines.append("| 订单号 | 错误信息 |") report_lines.append("| 订单号 | 错误信息 |")
report_lines.append("|--------|----------|") report_lines.append("|--------|----------|")
for item in self.stats['errors']: for item in self.stats["errors"]:
report_lines.append(f"| {item['order_id']} | {item['error_message']} |") report_lines.append(f"| {item['order_id']} | {item['error_message']} |")
report_lines.append("") report_lines.append("")

View File

@@ -27,22 +27,24 @@ except ImportError:
# --- 全局日志配置 --- # --- 全局日志配置 ---
# 调整格式:增加 [] 使其与 UI 控件的默认风格保持一致 # 调整格式:增加 [] 使其与 UI 控件的默认风格保持一致
LOG_FORMAT = '[%(asctime)s] [%(levelname)s] %(message)s' LOG_FORMAT = "[%(asctime)s] [%(levelname)s] %(message)s"
DATE_FORMAT = '%Y-%m-%d %H:%M:%S' DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
logging.basicConfig( logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT)
level=logging.INFO,
format=LOG_FORMAT,
datefmt=DATE_FORMAT
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DiscreteMaterialPlanExtractor: class DiscreteMaterialPlanExtractor:
"""离散备料计划维护数据提取器""" """离散备料计划维护数据提取器"""
def __init__( def __init__(
self, username, password, headless=False, verbose=True, batch_size=100, self,
enable_db_persistence=False username,
password,
headless=False,
verbose=True,
batch_size=100,
enable_db_persistence=False,
): ):
self.username = username self.username = username
self.password = password self.password = password
@@ -57,6 +59,7 @@ class DiscreteMaterialPlanExtractor:
if self.enable_db_persistence: if self.enable_db_persistence:
try: try:
from db.discrete_material_plan_dao import DiscreteMaterialPlanDAO from db.discrete_material_plan_dao import DiscreteMaterialPlanDAO
self.dao = DiscreteMaterialPlanDAO() self.dao = DiscreteMaterialPlanDAO()
except ImportError: except ImportError:
self._log("无法加载数据库 DAO 模块,持久化功能将不可用", "error") self._log("无法加载数据库 DAO 模块,持久化功能将不可用", "error")
@@ -67,11 +70,7 @@ class DiscreteMaterialPlanExtractor:
""" """
level = level.lower() level = level.lower()
# 1. 记录到标准控制台 # 1. 记录到标准控制台
log_map = { log_map = {"info": logger.info, "warn": logger.warning, "error": logger.error}
"info": logger.info,
"warn": logger.warning,
"error": logger.error
}
log_func = log_map.get(level, logger.info) log_func = log_map.get(level, logger.info)
log_func(message) log_func(message)
@@ -80,7 +79,9 @@ class DiscreteMaterialPlanExtractor:
if self.progress_callback: if self.progress_callback:
self._report_progress("log", 0, 0, message, log_level=level.upper()) self._report_progress("log", 0, 0, message, log_level=level.upper())
def _report_progress(self, stage: str, current: int, total: int, message: str, **detail): def _report_progress(
self, stage: str, current: int, total: int, message: str, **detail
):
"""标准化进度汇报""" """标准化进度汇报"""
if self.progress_callback and ProgressInfo: if self.progress_callback and ProgressInfo:
try: try:
@@ -98,19 +99,29 @@ class DiscreteMaterialPlanExtractor:
def get_production_order_numbers(self, production_id_file, report_progress=False): def get_production_order_numbers(self, production_id_file, report_progress=False):
"""读取总排号并查询数据库获取生产订单号""" """读取总排号并查询数据库获取生产订单号"""
if report_progress: if report_progress:
self._report_progress("query", 1, 3, "正在读取总排号文件...", action="read_file") self._report_progress(
"query", 1, 3, "正在读取总排号文件...", action="read_file"
)
production_ids = read_production_ids(production_id_file) production_ids = read_production_ids(production_id_file)
self._log(f"文件读取完成: 找到 {len(production_ids)} 个 Production ID") self._log(f"文件读取完成: 找到 {len(production_ids)} 个 Production ID")
if report_progress: if report_progress:
self._report_progress("query", 2, 3, "正在查询数据库获取生产订单号...", action="query_database") self._report_progress(
"query",
2,
3,
"正在查询数据库获取生产订单号...",
action="query_database",
)
order_ids = query_production_order_numbers(production_ids) order_ids = query_production_order_numbers(production_ids)
self._log(f"数据库查询完成: 共匹配到 {len(order_ids)} 条生产订单号") self._log(f"数据库查询完成: 共匹配到 {len(order_ids)} 条生产订单号")
if report_progress: if report_progress:
self._report_progress("query", 3, 3, "订单号查询阶段结束", action="query_complete") self._report_progress(
"query", 3, 3, "订单号查询阶段结束", action="query_complete"
)
return order_ids return order_ids
@@ -121,8 +132,13 @@ class DiscreteMaterialPlanExtractor:
def download_batch(self, inner_frame, order_ids, batch_index, total_batches, page1): def download_batch(self, inner_frame, order_ids, batch_index, total_batches, page1):
"""执行单批次数据的下载流程""" """执行单批次数据的下载流程"""
self._report_progress("download", batch_index * 7 + 1, total_batches * 7, self._report_progress(
f"{batch_index + 1} 批: 正在填充订单号", action="fill_orders") "download",
batch_index * 7 + 1,
total_batches * 7,
f"{batch_index + 1} 批: 正在填充订单号",
action="fill_orders",
)
textbox = inner_frame.get_by_role("textbox", name="来源生产订单号") textbox = inner_frame.get_by_role("textbox", name="来源生产订单号")
textbox.fill("") textbox.fill("")
@@ -140,7 +156,11 @@ class DiscreteMaterialPlanExtractor:
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 = (
inner_frame.locator("div")
.filter(has_text=re.compile(r"^行数阈值$"))
.locator("input[type='text']")
)
threshold_box.fill("300000") threshold_box.fill("300000")
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"
@@ -165,26 +185,36 @@ class DiscreteMaterialPlanExtractor:
total_steps = len(file_paths) * 2 + 3 total_steps = len(file_paths) * 2 + 3
for i, path in enumerate(file_paths, 1): for i, path in enumerate(file_paths, 1):
self._report_progress("convert", 1 + (i-1)*2 + 1, total_steps, f"正在转换 Excel {i}/{len(file_paths)}") self._report_progress(
"convert",
1 + (i - 1) * 2 + 1,
total_steps,
f"正在转换 Excel {i}/{len(file_paths)}",
)
df = self.converter.convert(path, output_file=None) df = self.converter.convert(path, output_file=None)
all_dfs.append(df) all_dfs.append(df)
self._log(f"文件 {i} 转换完成: 提取到 {len(df)} 条记录") self._log(f"文件 {i} 转换完成: 提取到 {len(df)} 条记录")
if all_dfs: if all_dfs:
self._report_progress("convert", total_steps - 1, total_steps, "正在进行最终数据合并...") self._report_progress(
"convert", total_steps - 1, total_steps, "正在进行最终数据合并..."
)
merged_df = pd.concat(all_dfs, ignore_index=True) merged_df = pd.concat(all_dfs, ignore_index=True)
merged_df.to_excel(output_path, index=False) merged_df.to_excel(output_path, index=False)
for p in file_paths: for p in file_paths:
try: os.remove(p) try:
except: pass os.remove(p)
except:
pass
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):
"""将结果存入数据库并打印详细统计信息""" """将结果存入数据库并打印详细统计信息"""
if not self.dao: return if not self.dao:
return
try: try:
self._report_progress("database", 1, 3, "正在将数据同步至数据库...") self._report_progress("database", 1, 3, "正在将数据同步至数据库...")
# 使用 with 关键字确保资源安全释放 # 使用 with 关键字确保资源安全释放
@@ -209,8 +239,10 @@ class DiscreteMaterialPlanExtractor:
input_box.press("Enter") input_box.press("Enter")
def extract( def extract(
self, production_id_file, output_file="D:/python/playwrite/data/离散备料计划维护_合并.xlsx", self,
progress_callback=None production_id_file,
output_file="D:/python/playwrite/data/离散备料计划维护_合并.xlsx",
progress_callback=None,
): ):
"""主入口:执行全流程数据提取任务""" """主入口:执行全流程数据提取任务"""
self.progress_callback = progress_callback self.progress_callback = progress_callback
@@ -220,15 +252,22 @@ class DiscreteMaterialPlanExtractor:
with sync_playwright() as playwright: with sync_playwright() as playwright:
self._report_progress("login", 1, 3, "启动浏览器并尝试登录 ERP...") self._report_progress("login", 1, 3, "启动浏览器并尝试登录 ERP...")
browser, context, page, main_frame = login( browser, context, page, main_frame = login(
playwright=playwright, username=self.username, password=self.password, playwright=playwright,
headless=self.headless, ignore_https_errors=True username=self.username,
password=self.password,
headless=self.headless,
ignore_https_errors=True,
) )
self._log("======================================== 开始执行数据提取任务 ========================================") self._log(
"======================================== 开始执行数据提取任务 ========================================"
)
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
f_frame = page1.locator("#forwardFrame").content_frame f_frame = page1.locator("#forwardFrame").content_frame
@@ -237,16 +276,22 @@ class DiscreteMaterialPlanExtractor:
work_frame = inner_frame_locator.content_frame work_frame = inner_frame_locator.content_frame
self.setup_query_interface(work_frame) self.setup_query_interface(work_frame)
order_ids = self.get_production_order_numbers(production_id_file, report_progress=True) order_ids = self.get_production_order_numbers(
production_id_file, report_progress=True
)
batch_list = list(self.group_order_ids(order_ids, self.batch_size)) batch_list = list(self.group_order_ids(order_ids, self.batch_size))
for i, batch_ids in enumerate(batch_list): for i, batch_ids in enumerate(batch_list):
self._log(f"正在处理第 {i+1} 批次 (共 {len(batch_list)} 批)") self._log(f"正在处理第 {i+1} 批次 (共 {len(batch_list)} 批)")
try: try:
f_path = self.download_batch(work_frame, batch_ids, i, len(batch_list), page1) f_path = self.download_batch(
work_frame, batch_ids, i, len(batch_list), page1
)
downloaded_files.append(f_path) downloaded_files.append(f_path)
except Exception as e: except Exception as e:
self._log(f"批次 {i+1} 处理异常,已跳过。详细错误: {e}", "error") self._log(
f"批次 {i+1} 处理异常,已跳过。详细错误: {e}", "error"
)
continue continue
self._log("正在注销并关闭浏览器环境...") self._log("正在注销并关闭浏览器环境...")
@@ -255,7 +300,9 @@ class DiscreteMaterialPlanExtractor:
browser.close() browser.close()
if downloaded_files: if downloaded_files:
final_path, final_df = self.convert_and_merge_files(downloaded_files, output_file) final_path, final_df = self.convert_and_merge_files(
downloaded_files, output_file
)
if self.enable_db_persistence and final_df is not None: if self.enable_db_persistence and final_df is not None:
self._save_to_database(final_df) self._save_to_database(final_df)
@@ -269,14 +316,16 @@ class DiscreteMaterialPlanExtractor:
finally: finally:
self.progress_callback = None self.progress_callback = None
def main(): def main():
extractor = DiscreteMaterialPlanExtractor( extractor = DiscreteMaterialPlanExtractor(
username="BLDpengqiangqiang", username="BLDpengqiangqiang",
password="your_password", password="your_password",
enable_db_persistence=True enable_db_persistence=True,
) )
id_file = os.path.join(os.path.dirname(__file__), "productionID.txt") id_file = os.path.join(os.path.dirname(__file__), "productionID.txt")
extractor.extract(id_file) extractor.extract(id_file)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -23,19 +23,22 @@ from db.discrete_material_plan_dao import DiscreteMaterialPlanDAO
from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO
from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO
# ==================== DATA STRUCTURES ==================== # ==================== DATA STRUCTURES ====================
@dataclass @dataclass
class MaterialValidationResult: class MaterialValidationResult:
"""Enhanced material validation result with complete record information""" """Enhanced material validation result with complete record information"""
material_name: str material_name: str
material_code: str material_code: str
specification: Optional[str] = None specification: Optional[str] = None
model: Optional[str] = None model: Optional[str] = None
manager_name: Optional[str] = None manager_name: Optional[str] = None
is_marked_for_deletion: bool = False is_marked_for_deletion: bool = False
matched_type_keyword: Optional[str] = None # Matched keyword from MaterialsTypeToBeDeleted matched_type_keyword: Optional[str] = (
None # Matched keyword from MaterialsTypeToBeDeleted
)
class MaterialStatusValidator: class MaterialStatusValidator:
@@ -261,13 +264,11 @@ class MaterialStatusValidator:
Returns: Returns:
List[str]: 输入项列表(可能是总排号或生产订单号) List[str]: 输入项列表(可能是总排号或生产订单号)
""" """
with open(production_id_file, 'r', encoding='utf-8') as f: with open(production_id_file, "r", encoding="utf-8") as f:
items = [line.strip() for line in f if line.strip()] items = [line.strip() for line in f if line.strip()]
return items return items
def _get_source_numbers_from_inputs( def _get_source_numbers_from_inputs(self, inputs: List[str]) -> List[str]:
self, inputs: List[str]
) -> List[str]:
""" """
根据输入列表智能获取 SourceNumber生产订单号列表 根据输入列表智能获取 SourceNumber生产订单号列表
@@ -281,7 +282,7 @@ class MaterialStatusValidator:
List[str]: 生产订单号列表 List[str]: 生产订单号列表
""" """
production_ids = [] # 需要查询数据库的 production_ids = [] # 需要查询数据库的
order_numbers = [] # 直接使用的 order_numbers = [] # 直接使用的
for item in inputs: for item in inputs:
input_type = self._identify_input_type(item) input_type = self._identify_input_type(item)
@@ -298,7 +299,9 @@ class MaterialStatusValidator:
# 查询数据库获取总排号对应的生产订单号 # 查询数据库获取总排号对应的生产订单号
if production_ids: if production_ids:
self._print(f"[INFO] 正在查询 {len(production_ids)} 个总排号对应的生产订单号...") self._print(
f"[INFO] 正在查询 {len(production_ids)} 个总排号对应的生产订单号..."
)
contract_dao = ProductionContractDataDAO() contract_dao = ProductionContractDataDAO()
db_order_numbers = contract_dao.get_source_numbers_by_总排号(production_ids) db_order_numbers = contract_dao.get_source_numbers_by_总排号(production_ids)
self._print(f"[INFO] 从数据库获取到 {len(db_order_numbers)} 个生产订单号") self._print(f"[INFO] 从数据库获取到 {len(db_order_numbers)} 个生产订单号")
@@ -307,7 +310,9 @@ class MaterialStatusValidator:
# 去重 # 去重
unique_order_numbers = list(dict.fromkeys(order_numbers)) unique_order_numbers = list(dict.fromkeys(order_numbers))
if len(unique_order_numbers) != len(order_numbers): if len(unique_order_numbers) != len(order_numbers):
self._print(f"[INFO] 去重后得到 {len(unique_order_numbers)} 个唯一生产订单号") self._print(
f"[INFO] 去重后得到 {len(unique_order_numbers)} 个唯一生产订单号"
)
return unique_order_numbers return unique_order_numbers
@@ -326,7 +331,9 @@ class MaterialStatusValidator:
if source_numbers is None or not source_numbers: if source_numbers is None or not source_numbers:
self._print("[INFO] 查询所有材料的名称...") self._print("[INFO] 查询所有材料的名称...")
else: else:
self._print(f"[INFO] 查询 {len(source_numbers)} 个生产订单对应的材料名称...") self._print(
f"[INFO] 查询 {len(source_numbers)} 个生产订单对应的材料名称..."
)
dao = DiscreteMaterialPlanDAO() dao = DiscreteMaterialPlanDAO()
material_names = dao.get_unique_material_names(source_numbers) material_names = dao.get_unique_material_names(source_numbers)
@@ -338,7 +345,7 @@ class MaterialStatusValidator:
self, self,
production_id_file: str = None, production_id_file: str = None,
full_table: bool = False, full_table: bool = False,
output_file: str = None output_file: str = None,
) -> str: ) -> str:
""" """
使用数据库作为数据源执行校验 使用数据库作为数据源执行校验
@@ -392,9 +399,7 @@ class MaterialStatusValidator:
# 3. 获取材料名称 # 3. 获取材料名称
material_names = self._get_material_names_from_db(source_numbers) material_names = self._get_material_names_from_db(source_numbers)
else: else:
raise ValueError( raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数")
"必须指定 full_table=True 或提供 production_id_file 参数"
)
# 从数据库获取待删除物料 # 从数据库获取待删除物料
self._print("\n从数据库获取待删除物料...") self._print("\n从数据库获取待删除物料...")
@@ -426,7 +431,9 @@ class MaterialStatusValidator:
self, self,
material_records: List[Dict[str, Any]], material_records: List[Dict[str, Any]],
type_keywords: List[Dict[str, Any]], type_keywords: List[Dict[str, Any]],
marked_codes_dict: Dict[str, str] # Changed: MaterialCode -> ManagerName mapping marked_codes_dict: Dict[
str, str
], # Changed: MaterialCode -> ManagerName mapping
) -> List[MaterialValidationResult]: ) -> List[MaterialValidationResult]:
""" """
Match materials with detailed information. Match materials with detailed information.
@@ -442,14 +449,16 @@ class MaterialStatusValidator:
results = [] results = []
for record in material_records: for record in material_records:
material_name = record.get('MaterialName', '') or '' material_name = record.get("MaterialName", "") or ""
material_code = record.get('MaterialCode', '') or '' material_code = record.get("MaterialCode", "") or ""
specification = record.get('Specification', '') or None specification = record.get("Specification", "") or None
model = record.get('Model', '') or None model = record.get("Model", "") or None
# Priority 1: Check MaterialsToBeDeleted (MaterialCode exact match) # Priority 1: Check MaterialsToBeDeleted (MaterialCode exact match)
# This has highest priority - if MaterialCode exists, use its ManagerName # This has highest priority - if MaterialCode exists, use its ManagerName
manager_name = marked_codes_dict.get(material_code) if material_code else None manager_name = (
marked_codes_dict.get(material_code) if material_code else None
)
is_marked = manager_name is not None is_marked = manager_name is not None
matched_keyword = None matched_keyword = None
@@ -457,10 +466,10 @@ class MaterialStatusValidator:
# (MaterialName contains match) # (MaterialName contains match)
if not manager_name: if not manager_name:
for type_record in type_keywords: for type_record in type_keywords:
type_material_name = type_record.get('MaterialName', '') type_material_name = type_record.get("MaterialName", "")
if type_material_name and type_material_name in material_name: if type_material_name and type_material_name in material_name:
matched_keyword = type_material_name matched_keyword = type_material_name
manager_name = type_record.get('ManagerName') manager_name = type_record.get("ManagerName")
break break
result = MaterialValidationResult( result = MaterialValidationResult(
@@ -470,7 +479,7 @@ class MaterialStatusValidator:
model=model, model=model,
manager_name=manager_name, manager_name=manager_name,
is_marked_for_deletion=is_marked, is_marked_for_deletion=is_marked,
matched_type_keyword=matched_keyword matched_type_keyword=matched_keyword,
) )
results.append(result) results.append(result)
@@ -480,7 +489,7 @@ class MaterialStatusValidator:
self, self,
production_id_file: str = None, production_id_file: str = None,
full_table: bool = False, full_table: bool = False,
output_file: str = None output_file: str = None,
) -> tuple: ) -> tuple:
""" """
Enhanced database validation with complete record information. Enhanced database validation with complete record information.
@@ -510,7 +519,9 @@ class MaterialStatusValidator:
# Get material records (complete records, not just MaterialName) # Get material records (complete records, not just MaterialName)
if full_table: if full_table:
self._print("\n模式: 全表校验") self._print("\n模式: 全表校验")
self._print("[INFO] 查询 DiscreteMaterialPlanData 表中的所有完整记录(启用 MaterialCode 去重)...") self._print(
"[INFO] 查询 DiscreteMaterialPlanData 表中的所有完整记录(启用 MaterialCode 去重)..."
)
dao = DiscreteMaterialPlanDAO() dao = DiscreteMaterialPlanDAO()
# Get original count for deduplication statistics # Get original count for deduplication statistics
@@ -521,7 +532,9 @@ class MaterialStatusValidator:
self._print(f"[INFO] 获取到 {len(material_records)} 条记录") self._print(f"[INFO] 获取到 {len(material_records)} 条记录")
if dedup_count > 0: if dedup_count > 0:
self._print(f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录") self._print(
f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录"
)
elif production_id_file: elif production_id_file:
self._print("\n模式: 输入过滤校验") self._print("\n模式: 输入过滤校验")
self._print(f"[INFO] 读取输入文件: {production_id_file}") self._print(f"[INFO] 读取输入文件: {production_id_file}")
@@ -542,7 +555,9 @@ class MaterialStatusValidator:
return output_file, [] return output_file, []
# 3. Get complete material records with deduplication # 3. Get complete material records with deduplication
self._print(f"[INFO] 查询 {len(source_numbers)} 个生产订单对应的完整物料记录(启用 MaterialCode 去重)...") self._print(
f"[INFO] 查询 {len(source_numbers)} 个生产订单对应的完整物料记录(启用 MaterialCode 去重)..."
)
dao = DiscreteMaterialPlanDAO() dao = DiscreteMaterialPlanDAO()
# Get original count for deduplication statistics # Get original count for deduplication statistics
@@ -554,7 +569,9 @@ class MaterialStatusValidator:
self._print(f"[INFO] 获取到 {len(material_records)} 条记录") self._print(f"[INFO] 获取到 {len(material_records)} 条记录")
if dedup_count > 0: if dedup_count > 0:
self._print(f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录") self._print(
f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录"
)
# 如果没有找到物料记录,给出友好提示 # 如果没有找到物料记录,给出友好提示
if not material_records: if not material_records:
@@ -563,7 +580,9 @@ class MaterialStatusValidator:
self._print("[ERROR] 1. 这些生产订单的物料数据还没有提取到数据库") self._print("[ERROR] 1. 这些生产订单的物料数据还没有提取到数据库")
self._print("[ERROR] 2. 请先运行【正式备料计划数据提取】工具") self._print("[ERROR] 2. 请先运行【正式备料计划数据提取】工具")
self._print("[ERROR] 3. 提取时勾选【持久化到数据库】选项") self._print("[ERROR] 3. 提取时勾选【持久化到数据库】选项")
self._print(f"[ERROR] 4. 将这些输入项的物料数据保存到 DiscreteMaterialPlanData 表") self._print(
f"[ERROR] 4. 将这些输入项的物料数据保存到 DiscreteMaterialPlanData 表"
)
else: else:
raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数") raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数")
@@ -580,15 +599,17 @@ class MaterialStatusValidator:
# Build dictionary: MaterialCode -> ManagerName # Build dictionary: MaterialCode -> ManagerName
marked_codes_dict = { marked_codes_dict = {
r['MaterialCode']: r['ManagerName'] r["MaterialCode"]: r["ManagerName"]
for r in marked_records for r in marked_records
if r.get('MaterialCode') and r.get('ManagerName') if r.get("MaterialCode") and r.get("ManagerName")
} }
self._print(f"获取到 {len(marked_codes_dict)} 个已标记的物料代码") self._print(f"获取到 {len(marked_codes_dict)} 个已标记的物料代码")
# Match materials # Match materials
self._print("\n匹配物料...") self._print("\n匹配物料...")
results = self.match_materials_detailed(material_records, type_keywords, marked_codes_dict) results = self.match_materials_detailed(
material_records, type_keywords, marked_codes_dict
)
# Output to Excel # Output to Excel
self._print("\n输出结果...") self._print("\n输出结果...")
@@ -596,15 +617,17 @@ class MaterialStatusValidator:
# Convert to DataFrame for Excel export # Convert to DataFrame for Excel export
df_data = [] df_data = []
for r in results: for r in results:
df_data.append({ df_data.append(
"材料名称": r.material_name, {
"材料代码": r.material_code, "材料名称": r.material_name,
"规格": r.specification or '', "材料代码": r.material_code,
"型号": r.model or '', "规格": r.specification or "",
"负责人": r.manager_name or '', "型号": r.model or "",
"已标记删除": "" if r.is_marked_for_deletion else "", "负责人": r.manager_name or "",
"匹配的关键词": r.matched_type_keyword or '' "已标记删除": "" if r.is_marked_for_deletion else "",
}) "匹配的关键词": r.matched_type_keyword or "",
}
)
result_df = pd.DataFrame(df_data) result_df = pd.DataFrame(df_data)
result_df.to_excel(output_file, index=False) result_df.to_excel(output_file, index=False)