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:
@@ -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"]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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 文件
|
||||||
|
|
||||||
|
|||||||
@@ -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"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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]]:
|
||||||
"""
|
"""
|
||||||
执行查询语句并返回结果
|
执行查询语句并返回结果
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}%",))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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("生产订单号")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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']};"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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", # 灰色
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -109,4 +109,4 @@ DEFAULT_DB_BATCH_SIZE = 2000
|
|||||||
CHECKBOX_CHECKED = "☑"
|
CHECKBOX_CHECKED = "☑"
|
||||||
|
|
||||||
# 未选中状态
|
# 未选中状态
|
||||||
CHECKBOX_UNCHECKED = "☐"
|
CHECKBOX_UNCHECKED = "☐"
|
||||||
|
|||||||
@@ -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,8 +273,10 @@ 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()
|
||||||
self._on_production_ids_changed()
|
self._on_production_ids_changed()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
# 更新状态栏
|
# 更新状态栏
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 # 显示给用户的消息
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
22
gui/utils.py
22
gui/utils.py
@@ -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__} 需要有效会话")
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,35 +208,33 @@ 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:
|
||||||
# 移除箭头,显示原始标题
|
# 移除箭头,显示原始标题
|
||||||
self.heading(col, text=original)
|
self.heading(col, text=original)
|
||||||
|
|||||||
@@ -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):
|
||||||
"""关闭窗口"""
|
"""关闭窗口"""
|
||||||
@@ -444,4 +444,4 @@ class DeleteProgressWindow:
|
|||||||
def set_completed(self):
|
def set_completed(self):
|
||||||
"""设置为完成状态"""
|
"""设置为完成状态"""
|
||||||
self.cancel_button.pack_forget()
|
self.cancel_button.pack_forget()
|
||||||
self.close_button.pack(side=tk.RIGHT)
|
self.close_button.pack(side=tk.RIGHT)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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("")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -53,10 +55,11 @@ class DiscreteMaterialPlanExtractor:
|
|||||||
self.converter = ExcelConverter(verbose=verbose)
|
self.converter = ExcelConverter(verbose=verbose)
|
||||||
self.enable_db_persistence = enable_db_persistence
|
self.enable_db_persistence = enable_db_persistence
|
||||||
self.dao = None
|
self.dao = None
|
||||||
|
|
||||||
if self.enable_db_persistence:
|
if self.enable_db_persistence:
|
||||||
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,20 +99,30 @@ 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
|
||||||
|
|
||||||
def group_order_ids(self, order_ids, group_size=100):
|
def group_order_ids(self, order_ids, group_size=100):
|
||||||
@@ -121,9 +132,14 @@ 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("")
|
||||||
textbox.fill(",".join(order_ids))
|
textbox.fill(",".join(order_ids))
|
||||||
@@ -139,8 +155,12 @@ class DiscreteMaterialPlanExtractor:
|
|||||||
inner_frame.get_by_role("row", name="序号").get_by_label("").click()
|
inner_frame.get_by_role("row", name="序号").get_by_label("").click()
|
||||||
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,36 +185,46 @@ 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 关键字确保资源安全释放
|
||||||
with self.dao as db:
|
with self.dao as db:
|
||||||
stats = db.save_dataframe_with_replace(df)
|
stats = db.save_dataframe_with_replace(df)
|
||||||
|
|
||||||
# 保留并输出完整的处理细节:删除条数和新增条数
|
# 保留并输出完整的处理细节:删除条数和新增条数
|
||||||
msg = f"数据库保存完成: 删除 {stats.get('deleted', 0)} 条, 新增 {stats.get('inserted', 0)} 条"
|
msg = f"数据库保存完成: 删除 {stats.get('deleted', 0)} 条, 新增 {stats.get('inserted', 0)} 条"
|
||||||
self._log(msg, "info")
|
self._log(msg, "info")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._log(f"数据库保存失败: {str(e)}", "error")
|
self._log(f"数据库保存失败: {str(e)}", "error")
|
||||||
|
|
||||||
@@ -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,28 +300,32 @@ 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)
|
||||||
|
|
||||||
self._log(f"所有流程已顺利结束,结果文件: {final_path}")
|
self._log(f"所有流程已顺利结束,结果文件: {final_path}")
|
||||||
self._report_progress("complete", 1, 1, "任务完成")
|
self._report_progress("complete", 1, 1, "任务完成")
|
||||||
return final_path
|
return final_path
|
||||||
|
|
||||||
self._log("未获得任何有效数据,任务终止", "warn")
|
self._log("未获得任何有效数据,任务终止", "warn")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user