From 3b7c00377f22f43dfe5311b1f2c05e4f55753bba Mon Sep 17 00:00:00 2001 From: Misaka Date: Thu, 26 Feb 2026 22:44:03 +0800 Subject: [PATCH] style: format all Python files with Black Apply Black formatter to the entire codebase for consistent code style. Co-Authored-By: Claude (glm-5) --- auth/__init__.py | 3 +- auth/session_manager.py | 27 +- config/defaults.py | 2 +- config/env_loader.py | 5 +- config/loader.py | 49 ++- config/schema.py | 39 ++- db/base_connection.py | 4 +- db/base_dao.py | 3 +- db/bip_users_dao.py | 47 +-- db/connection.py | 1 + db/connection_factory.py | 29 +- db/discrete_material_plan_dao.py | 118 ++++--- db/materials_to_be_deleted_dao.py | 59 ++-- db/materials_to_be_deleted_records_dao.py | 96 +++--- db/materials_to_delete.py | 8 +- db/mysql_connection.py | 20 +- db/production_contract_data_dao.py | 18 +- db/production_order_query.py | 6 +- db/sqlserver_connection.py | 2 +- db/table_name_converter.py | 15 +- gui/__init__.py | 18 +- gui/base_tab.py | 14 +- gui/config_manager.py | 9 +- gui/constants.py | 12 +- gui/data_extraction_tab.py | 71 +++-- gui/log_config.py | 7 +- gui/login_dialog.py | 28 +- gui/main_window.py | 26 +- gui/material_type_management_dialog.py | 313 +++++++++++-------- gui/material_validation_tab.py | 362 ++++++++++++++-------- gui/progress.py | 4 +- gui/settings_tab.py | 182 ++++++++--- gui/user_selection_dialog.py | 28 +- gui/utils.py | 22 +- gui/widgets/__init__.py | 12 +- gui/widgets/checkbox_treeview.py | 46 ++- gui/widgets/delete_progress_window.py | 140 ++++----- gui/widgets/file_selector.py | 11 +- gui/widgets/log_handler.py | 18 +- gui/widgets/log_text.py | 57 ++-- gui/widgets/production_id_input.py | 11 +- gui/widgets/progress_dialog.py | 16 +- scripts/migrate_to_env.py | 22 +- utils/discrete_material_plan_cleaner.py | 228 ++++++++------ utils/discrete_material_plan_extractor.py | 149 ++++++--- utils/material_status_validator.py | 105 ++++--- 46 files changed, 1488 insertions(+), 974 deletions(-) diff --git a/auth/__init__.py b/auth/__init__.py index 2f85cc9..22fb92c 100644 --- a/auth/__init__.py +++ b/auth/__init__.py @@ -1,6 +1,7 @@ """ Auth package for user authentication and session management """ + from .session_manager import SessionManager -__all__ = ['SessionManager'] +__all__ = ["SessionManager"] diff --git a/auth/session_manager.py b/auth/session_manager.py index bd22a57..939a879 100644 --- a/auth/session_manager.py +++ b/auth/session_manager.py @@ -1,6 +1,7 @@ """ Session Manager - Singleton pattern for managing authenticated user session """ + from typing import Optional, Dict, Any @@ -28,7 +29,7 @@ class SessionManager: self._initialized = True @classmethod - def get_instance(cls) -> 'SessionManager': + def get_instance(cls) -> "SessionManager": """ Get the singleton instance of SessionManager @@ -57,8 +58,8 @@ class SessionManager: if user_info: self._current_user = { - 'username': user_info['username'], - 'user_type': user_info['user_type'] + "username": user_info["username"], + "user_type": user_info["user_type"], } return True return False @@ -79,8 +80,8 @@ class SessionManager: if user_info: self._current_user = { - 'username': user_info['username'], - 'user_type': user_info['user_type'] + "username": user_info["username"], + "user_type": user_info["user_type"], } return True return False @@ -107,7 +108,7 @@ class SessionManager: """ if not self._current_user: return False - return self._current_user.get('user_type') == 'Admin' + return self._current_user.get("user_type") == "Admin" def is_guest(self) -> bool: """ @@ -118,7 +119,7 @@ class SessionManager: """ if not self._current_user: return False - return self._current_user.get('user_type') == 'Guest' + return self._current_user.get("user_type") == "Guest" def get_username(self) -> Optional[str]: """ @@ -129,7 +130,7 @@ class SessionManager: """ if not self._current_user: return None - return self._current_user.get('username') + return self._current_user.get("username") def get_user_type(self) -> Optional[str]: """ @@ -140,7 +141,7 @@ class SessionManager: """ if not self._current_user: return None - return self._current_user.get('user_type') + return self._current_user.get("user_type") def get_user_info(self) -> Optional[dict]: """ @@ -168,12 +169,12 @@ class SessionManager: return False # 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._current_user = { - 'username': user_info['username'], - 'user_type': user_info['user_type'] + "username": user_info["username"], + "user_type": user_info["user_type"], } return True @@ -184,4 +185,4 @@ class SessionManager: Returns: Original admin user dict if a switch occurred, None otherwise """ - return getattr(self, '_original_admin_user', None) + return getattr(self, "_original_admin_user", None) diff --git a/config/defaults.py b/config/defaults.py index 83b7a96..d5f1b8b 100644 --- a/config/defaults.py +++ b/config/defaults.py @@ -5,6 +5,7 @@ 定义所有配置项的默认值,从环境变量加载。 """ + from config.schema import ( ERPConfig, DatabaseConfig, @@ -17,7 +18,6 @@ from config.schema import ( DatabaseType, ) - # 默认配置 - 从环境变量加载 DEFAULT_APP_CONFIG = AppConfig.from_env() diff --git a/config/env_loader.py b/config/env_loader.py index f269587..7d43155 100644 --- a/config/env_loader.py +++ b/config/env_loader.py @@ -5,6 +5,7 @@ 使用 python-dotenv 加载 .env 文件,并提供类型转换功能。 """ + import os from pathlib import Path from typing import Any, Optional, Type, TypeVar @@ -111,7 +112,9 @@ def set_env(key: str, value: Any) -> None: 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 文件 diff --git a/config/loader.py b/config/loader.py index 9e259be..9d753bb 100644 --- a/config/loader.py +++ b/config/loader.py @@ -5,6 +5,7 @@ 负责加载、合并和验证配置,优先从环境变量加载。 """ + import json import os from typing import Any, Dict @@ -27,7 +28,9 @@ class ConfigLoader: """配置加载器""" @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_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_NAME": config.database.database, "DB_USERNAME": config.database.username, "DB_PASSWORD": config.database.password, # SQL Server 特定配置 - "DB_SQLSERVER_DRIVER": 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", + "DB_SQLSERVER_DRIVER": ( + 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 特定配置 - "DB_MYSQL_HOST": config.database.mysql.host if config.database.mysql else "", - "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", + "DB_MYSQL_HOST": ( + config.database.mysql.host if config.database.mysql else "" + ), + "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_PRODUCTION_ID_FILE": config.paths.production_id_file, @@ -199,7 +220,9 @@ class ConfigLoader: sqlserver_dict = database_dict.get("sqlserver", {}) sqlserver_config = SQLServerConfig( 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 配置 @@ -243,16 +266,18 @@ class ConfigLoader: verbose=extraction_dict.get("verbose", True), auto_convert=extraction_dict.get("auto_convert", 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( data_source=validation_dict.get("data_source", "database_full"), use_database=validation_dict.get("use_database", True), 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", ""), match_mode=validation_dict.get("match_mode", "substring"), ), ) - - diff --git a/config/schema.py b/config/schema.py index 2c342be..2812cf1 100644 --- a/config/schema.py +++ b/config/schema.py @@ -5,6 +5,7 @@ 使用 dataclass 定义所有配置项的结构和类型。 """ + from dataclasses import dataclass, field from typing import Optional from pathlib import Path @@ -13,6 +14,7 @@ from enum import Enum class DatabaseType(str, Enum): """数据库类型枚举""" + SQLSERVER = "sqlserver" MYSQL = "mysql" @@ -57,6 +59,7 @@ class ERPConfig: @dataclass class SQLServerConfig: """SQL Server 特定配置""" + driver: str = "ODBC Driver 18 for SQL Server" trust_server_certificate: str = "yes" @@ -74,6 +77,7 @@ class SQLServerConfig: @dataclass class MySQLConfig: """MySQL 特定配置""" + host: str = "" port: int = 3306 charset: str = "utf8mb4" @@ -167,7 +171,9 @@ class PathConfig: data_dir=get_env("PATH_DATA_DIR", "D:/python/playwrite/data/"), production_id_file=get_env("PATH_PRODUCTION_ID_FILE", "ProductionID.txt"), 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]: @@ -200,7 +206,9 @@ class ExtractionConfig: verbose=get_env_bool("EXTRACTION_VERBOSE", True), auto_convert=get_env_bool("EXTRACTION_AUTO_CONVERT", 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]: @@ -246,7 +254,7 @@ class ValidationConfig: "database_full", "database_filtered", "excel_existing", - "excel_full" + "excel_full", ] if self.data_source not in valid_sources: errors.append( @@ -281,6 +289,7 @@ class UIConfig: def from_env(cls) -> "UIConfig": """从环境变量创建配置""" from config.env_loader import get_env, get_env_int + return cls( font_family=get_env("UI_FONT_FAMILY", "Microsoft YaHei UI"), font_size=get_env_int("UI_FONT_SIZE", 10), @@ -366,19 +375,35 @@ class AppConfig: "auto_close_browser": self.erp.auto_close_browser, }, "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, "database": self.database.database, "username": self.database.username, "password": self.database.password, "sqlserver": { - "driver": 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", + "driver": ( + 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": { "host": self.database.mysql.host if self.database.mysql else "", "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": { diff --git a/db/base_connection.py b/db/base_connection.py index a703052..ec0ddf7 100644 --- a/db/base_connection.py +++ b/db/base_connection.py @@ -37,7 +37,9 @@ class BaseDatabaseConnection(ABC): pass @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]]: """ 执行查询语句并返回结果 diff --git a/db/base_dao.py b/db/base_dao.py index 4ad8222..008ee02 100644 --- a/db/base_dao.py +++ b/db/base_dao.py @@ -19,6 +19,7 @@ class BaseDAO: self.db: Optional[BaseDatabaseConnection] = None # 从配置文件加载数据库类型 from config.loader import ConfigLoader + app_config = ConfigLoader.load() self._db_type = app_config.database.db_type @@ -50,7 +51,7 @@ class BaseDAO: """ if self._db_type == DatabaseType.MYSQL: # SQL Server → MySQL - return TableNameConverter.convert_sql(sql, 'mysql') + return TableNameConverter.convert_sql(sql, "mysql") return sql def _get_placeholder(self) -> str: diff --git a/db/bip_users_dao.py b/db/bip_users_dao.py index 4cb489c..f7d4f45 100644 --- a/db/bip_users_dao.py +++ b/db/bip_users_dao.py @@ -1,6 +1,7 @@ """ BIPUsers DAO - Data access object for user authentication and management """ + from typing import Optional, Dict, Any, List from db.base_dao import BaseDAO from db.connection import get_connection @@ -22,7 +23,7 @@ class BIPUsersDAO(BaseDAO): Dict with user info if authentication successful, None otherwise Returns: {id, username, user_type} """ - table_name = self._convert_sql('[dbo].[BIPUsers]') + table_name = self._convert_sql("[dbo].[BIPUsers]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -43,13 +44,15 @@ class BIPUsersDAO(BaseDAO): results = db.execute_query(sql, (username, password)) if results: return { - 'id': results[0]['ID'], - 'username': results[0]['UserName'], - 'user_type': results[0]['UserType'] + "id": results[0]["ID"], + "username": results[0]["UserName"], + "user_type": results[0]["UserType"], } 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) @@ -60,7 +63,7 @@ class BIPUsersDAO(BaseDAO): Dict with user info if authentication successful, None otherwise Returns: {id, username, user_type} """ - table_name = self._convert_sql('[dbo].[BIPUsers]') + table_name = self._convert_sql("[dbo].[BIPUsers]") placeholder = self._get_placeholder() # Note: Column name is 'ComputerNmae' (typo in database schema) @@ -81,9 +84,9 @@ class BIPUsersDAO(BaseDAO): results = db.execute_query(sql, (computer_name,)) if results: return { - 'id': results[0]['ID'], - 'username': results[0]['UserName'], - 'user_type': results[0]['UserType'] + "id": results[0]["ID"], + "username": results[0]["UserName"], + "user_type": results[0]["UserType"], } return None @@ -94,7 +97,7 @@ class BIPUsersDAO(BaseDAO): Returns: 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: @@ -114,15 +117,17 @@ class BIPUsersDAO(BaseDAO): results = db.execute_query(sql) return [ { - 'id': row['ID'], - 'username': row['UserName'], - 'user_type': row['UserType'], - 'create_time': row['CreateTime'] + "id": row["ID"], + "username": row["UserName"], + "user_type": row["UserType"], + "create_time": row["CreateTime"], } 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 @@ -135,7 +140,7 @@ class BIPUsersDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[BIPUsers]') + table_name = self._convert_sql("[dbo].[BIPUsers]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -185,7 +190,7 @@ class BIPUsersDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[BIPUsers]') + table_name = self._convert_sql("[dbo].[BIPUsers]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -221,7 +226,7 @@ class BIPUsersDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[BIPUsers]') + table_name = self._convert_sql("[dbo].[BIPUsers]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -256,7 +261,7 @@ class BIPUsersDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[BIPUsers]') + table_name = self._convert_sql("[dbo].[BIPUsers]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -289,7 +294,7 @@ class BIPUsersDAO(BaseDAO): Returns: True if username exists, False otherwise """ - table_name = self._convert_sql('[dbo].[BIPUsers]') + table_name = self._convert_sql("[dbo].[BIPUsers]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -306,4 +311,4 @@ class BIPUsersDAO(BaseDAO): with get_connection() as db: results = db.execute_query(sql, (username,)) - return results[0]['count'] > 0 if results else False + return results[0]["count"] > 0 if results else False diff --git a/db/connection.py b/db/connection.py index d26b8ef..9a2163e 100644 --- a/db/connection.py +++ b/db/connection.py @@ -34,6 +34,7 @@ def get_connection(config=None) -> BaseDatabaseConnection: else: # 从用户配置文件加载 from config.loader import ConfigLoader + app_config = ConfigLoader.load() database_config = app_config.database diff --git a/db/connection_factory.py b/db/connection_factory.py index 010f6f6..3de5b81 100644 --- a/db/connection_factory.py +++ b/db/connection_factory.py @@ -16,8 +16,7 @@ class ConnectionFactory: @staticmethod def create_connection( - db_type: DatabaseType, - config: Optional[Dict[str, Any]] = None + db_type: DatabaseType, config: Optional[Dict[str, Any]] = None ) -> BaseDatabaseConnection: """ 根据数据库类型创建对应的连接实例 @@ -58,14 +57,14 @@ class ConnectionFactory: if db_type == DatabaseType.SQLSERVER: # 构建 SQL Server 配置字典 config = { - 'server': database_config.server, - 'database': database_config.database, - 'username': database_config.username, - 'password': database_config.password, + "server": database_config.server, + "database": database_config.database, + "username": database_config.username, + "password": database_config.password, } if database_config.sqlserver: - config['driver'] = database_config.sqlserver.driver - config['trust_server_certificate'] = ( + config["driver"] = database_config.sqlserver.driver + config["trust_server_certificate"] = ( database_config.sqlserver.trust_server_certificate ) return SQLServerConnection(config) @@ -73,17 +72,17 @@ class ConnectionFactory: elif db_type == DatabaseType.MYSQL: # 构建 MySQL 配置字典 config = { - 'database': database_config.database, - 'username': database_config.username, - 'password': database_config.password, + "database": database_config.database, + "username": database_config.username, + "password": database_config.password, } if database_config.mysql: - config['host'] = database_config.mysql.host - config['port'] = database_config.mysql.port - config['charset'] = database_config.mysql.charset + config["host"] = database_config.mysql.host + config["port"] = database_config.mysql.port + config["charset"] = database_config.mysql.charset else: # 回退到 server 字段(兼容旧配置) - config['host'] = database_config.server + config["host"] = database_config.server return MySQLConnection(config) else: diff --git a/db/discrete_material_plan_dao.py b/db/discrete_material_plan_dao.py index 884431c..220ba4b 100644 --- a/db/discrete_material_plan_dao.py +++ b/db/discrete_material_plan_dao.py @@ -37,19 +37,21 @@ class DiscreteMaterialPlanDAO(BaseDAO): ... print(f"Deleted: {stats['deleted']}, Inserted: {stats['inserted']}") """ if df.empty: - return {'deleted': 0, 'inserted': 0} + return {"deleted": 0, "inserted": 0} # Remove duplicates based on PlanNumber and SequenceNumber original_count = len(df) - df = df.drop_duplicates(subset=['备料计划单号', '序号'], keep='first') + df = df.drop_duplicates(subset=["备料计划单号", "序号"], keep="first") duplicates_removed = original_count - len(df) if duplicates_removed > 0: - print(f"[INFO] 检测到 {duplicates_removed} 条重复记录(相同计划单号和序号),已自动去重") + print( + f"[INFO] 检测到 {duplicates_removed} 条重复记录(相同计划单号和序号),已自动去重" + ) with get_connection() as db: # Get unique plan numbers - plan_numbers = df['备料计划单号'].unique().tolist() + plan_numbers = df["备料计划单号"].unique().tolist() # Delete existing records deleted = self._delete_by_plan_numbers(db, plan_numbers) @@ -57,7 +59,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): # Insert new records in batches 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: """ @@ -79,12 +81,12 @@ class DiscreteMaterialPlanDAO(BaseDAO): total_deleted = 0 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() - 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})" deleted = db.execute_update(sql, tuple(batch)) @@ -108,7 +110,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): Total number of records inserted """ # 根据数据库类型选择表名 - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") placeholder = self._get_placeholder() sql = f""" @@ -126,7 +128,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): records = self._convert_df_to_records(df) 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: db.execute_update(sql, record) total_inserted += 1 @@ -150,20 +152,44 @@ class DiscreteMaterialPlanDAO(BaseDAO): """ # Column order must match INSERT statement column_order = [ - '工厂', '备料状态', '备料计划单号', '来源单号', '备料类型', '产品编码', - '产品名称', '产品单位', '产品计划数量', '用料部门', '备注', '制单人', - '制单日期', '审批人', '审批日期', '序号', '材料编码', '材料名称', - '规格', '型号', '图号', '物料材质', '计划数量', '单位', '需用日期', - '发料仓库', '单位用量', '累计出库数量', 'BOM版本' + "工厂", + "备料状态", + "备料计划单号", + "来源单号", + "备料类型", + "产品编码", + "产品名称", + "产品单位", + "产品计划数量", + "用料部门", + "备注", + "制单人", + "制单日期", + "审批人", + "审批日期", + "序号", + "材料编码", + "材料名称", + "规格", + "型号", + "图号", + "物料材质", + "计划数量", + "单位", + "需用日期", + "发料仓库", + "单位用量", + "累计出库数量", + "BOM版本", ] # Numeric columns with their default values and data types numeric_columns = { - '产品计划数量': (0, int), - '序号': (0, int), - '计划数量': (0, int), - '单位用量': (0.0, float), - '累计出库数量': (0, int), + "产品计划数量": (0, int), + "序号": (0, int), + "计划数量": (0, int), + "单位用量": (0.0, float), + "累计出库数量": (0, int), } records = [] @@ -172,7 +198,11 @@ class DiscreteMaterialPlanDAO(BaseDAO): for col in column_order: value = row.get(col) # 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: # Use default value for numeric columns record.append(numeric_columns[col][0]) @@ -209,7 +239,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): List of dictionaries representing records """ with get_connection() as db: - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") placeholder = self._get_placeholder() sql = f"SELECT * FROM {table_name} WHERE PlanNumber = {placeholder}" return db.execute_query(sql, (plan_number,)) @@ -227,8 +257,8 @@ class DiscreteMaterialPlanDAO(BaseDAO): if not plan_numbers: return [] placeholder = self._get_placeholder() - placeholders = ','.join([placeholder for _ in plan_numbers]) - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + placeholders = ",".join([placeholder for _ in plan_numbers]) + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") sql = f"SELECT * FROM {table_name} WHERE PlanNumber IN ({placeholders})" with get_connection() as db: return db.execute_query(sql, tuple(plan_numbers)) @@ -244,7 +274,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): List of dictionaries representing records """ with get_connection() as db: - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") placeholder = self._get_placeholder() sql = f"SELECT * FROM {table_name} WHERE SourceNumber = {placeholder}" return db.execute_query(sql, (order_id,)) @@ -260,11 +290,11 @@ class DiscreteMaterialPlanDAO(BaseDAO): Number of records """ with get_connection() as db: - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") placeholder = self._get_placeholder() sql = f"SELECT COUNT(*) as count FROM {table_name} WHERE PlanNumber = {placeholder}" 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: """ @@ -274,10 +304,10 @@ class DiscreteMaterialPlanDAO(BaseDAO): Total number of records """ 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}" 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: """ @@ -301,7 +331,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): unique plans, unique orders, and date range """ with get_connection() as db: - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") sql = f""" SELECT COUNT(*) as total_records, @@ -324,7 +354,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): List of dictionaries representing all records """ 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}" return db.execute_query(sql) @@ -346,10 +376,10 @@ class DiscreteMaterialPlanDAO(BaseDAO): all_results = [] 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() - placeholders = ','.join([placeholder for _ in batch]) - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + placeholders = ",".join([placeholder for _ in batch]) + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") sql = f"SELECT * FROM {table_name} WHERE SourceNumber IN ({placeholders})" with get_connection() as db: results = db.execute_query(sql, tuple(batch)) @@ -368,7 +398,7 @@ class DiscreteMaterialPlanDAO(BaseDAO): List of dictionaries representing deduplicated records """ with get_connection() as db: - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") sql = f""" WITH RankedRecords AS ( @@ -414,10 +444,10 @@ class DiscreteMaterialPlanDAO(BaseDAO): all_results = [] 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() - placeholders = ','.join([placeholder for _ in batch]) - table_name = self._convert_sql('[dbo].[DiscreteMaterialPlanData]') + placeholders = ",".join([placeholder for _ in batch]) + table_name = self._convert_sql("[dbo].[DiscreteMaterialPlanData]") sql = f""" WITH RankedRecords AS ( @@ -458,23 +488,23 @@ class DiscreteMaterialPlanDAO(BaseDAO): Returns: 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: # No filter - get all unique material names sql = f"SELECT DISTINCT MaterialName FROM {table_name} WHERE MaterialName IS NOT NULL" with get_connection() as db: 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: # Filter by SourceNumber list batch_size = 2000 all_material_names = set() 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() - placeholders = ','.join([placeholder for _ in batch]) + placeholders = ",".join([placeholder for _ in batch]) sql = f""" SELECT DISTINCT MaterialName FROM {table_name} @@ -483,7 +513,9 @@ class DiscreteMaterialPlanDAO(BaseDAO): """ with get_connection() as db: 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) return list(all_material_names) diff --git a/db/materials_to_be_deleted_dao.py b/db/materials_to_be_deleted_dao.py index fe868b4..da19d98 100644 --- a/db/materials_to_be_deleted_dao.py +++ b/db/materials_to_be_deleted_dao.py @@ -16,9 +16,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): # ==================== CREATE ==================== - def insert_material( - self, material_name: str, manager_name: str - ) -> bool: + def insert_material(self, material_name: str, manager_name: str) -> bool: """ Insert a single material record. @@ -29,7 +27,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -65,7 +63,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): if not materials: return 0 - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -100,7 +98,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: 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: @@ -131,7 +129,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: 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() # 根据数据库类型选择列名格式 @@ -160,7 +158,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: 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: @@ -180,7 +178,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): with get_connection() as db: 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]: """ @@ -193,15 +191,12 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): List of material names for the specified manager """ 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 ==================== def update_manager( - self, - material_name: str, - old_manager: str, - new_manager: str + self, material_name: str, old_manager: str, new_manager: str ) -> bool: """ Update manager for a specific material. @@ -214,7 +209,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -233,7 +228,9 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): try: 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 except Exception as e: print(f"Error updating manager: {e}") @@ -241,11 +238,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): # ==================== DELETE ==================== - def delete_material( - self, - material_name: str, - manager_name: str - ) -> bool: + def delete_material(self, material_name: str, manager_name: str) -> bool: """ Delete a specific material record. @@ -256,7 +249,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -289,7 +282,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: Number of records deleted """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -318,7 +311,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: Number of records deleted """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") sql = f"DELETE FROM {table_name}" try: @@ -340,7 +333,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: True if material exists, False otherwise """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -359,7 +352,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): with get_connection() as db: 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: """ @@ -371,7 +364,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: Number of materials for the manager """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -390,7 +383,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): with get_connection() as db: 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]: """ @@ -400,7 +393,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Dictionary with statistics including total materials, 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: @@ -442,8 +435,8 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): # Get materials per manager manager_results = db.execute_query(manager_sql) - stats['materials_per_manager'] = [ - {r['ManagerName']: r['count']} for r in manager_results + stats["materials_per_manager"] = [ + {r["ManagerName"]: r["count"]} for r in manager_results ] return stats @@ -458,7 +451,7 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): Returns: List of matching materials """ - table_name = self._convert_sql('[dbo].[MaterialsTypeToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsTypeToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -478,4 +471,4 @@ class MaterialsTypeToBeDeletedDAO(BaseDAO): """ with get_connection() as db: - return db.execute_query(sql, (f'%{keyword}%',)) + return db.execute_query(sql, (f"%{keyword}%",)) diff --git a/db/materials_to_be_deleted_records_dao.py b/db/materials_to_be_deleted_records_dao.py index a02d54f..a110079 100644 --- a/db/materials_to_be_deleted_records_dao.py +++ b/db/materials_to_be_deleted_records_dao.py @@ -40,7 +40,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): with get_connection() as db: if self._db_type == DatabaseType.MYSQL: # MySQL 使用 INSERT ... ON DUPLICATE KEY UPDATE - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") placeholder = self._get_placeholder() if self._db_type == DatabaseType.MYSQL: @@ -57,7 +57,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): """ else: # SQL Server 使用 MERGE - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") sql = f""" MERGE {table_name} AS target 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); """ - 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 except Exception as e: print(f"Error upserting material: {e}") @@ -86,24 +92,26 @@ class MaterialsToBeDeletedDAO(BaseDAO): Dictionary with statistics: {'total': int, 'success': int, 'failed': int} """ 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: with get_connection() as db: for material in materials: - material_code = material.get('material_code', '').strip() - manager_name = material.get('manager_name', '') + material_code = material.get("material_code", "").strip() + manager_name = material.get("manager_name", "") if not material_code: - stats['failed'] += 1 + stats["failed"] += 1 continue try: if self._db_type == DatabaseType.MYSQL: # MySQL 使用 INSERT ... ON DUPLICATE KEY UPDATE - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql( + "[dbo].[MaterialsToBeDeleted]" + ) placeholder = self._get_placeholder() if self._db_type == DatabaseType.MYSQL: @@ -120,7 +128,9 @@ class MaterialsToBeDeletedDAO(BaseDAO): """ else: # SQL Server 使用 MERGE - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql( + "[dbo].[MaterialsToBeDeleted]" + ) sql = f""" MERGE {table_name} AS target 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); """ - db.execute_update(sql, (material_code, manager_name.strip() if manager_name else None)) - stats['success'] += 1 + db.execute_update( + sql, + ( + material_code, + manager_name.strip() if manager_name else None, + ), + ) + stats["success"] += 1 except Exception as e: print(f"Error upserting material {material_code}: {e}") - stats['failed'] += 1 + stats["failed"] += 1 except Exception as e: print(f"Error in batch upsert: {e}") - stats['failed'] = stats['total'] - stats['success'] + stats["failed"] = stats["total"] - stats["success"] return stats @@ -153,7 +169,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: Set of material codes """ - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") # 根据数据库类型选择列名格式 if self._db_type == DatabaseType.MYSQL: @@ -172,7 +188,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): try: with get_connection() as db: 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: print(f"Error getting material codes: {e}") return set() @@ -184,7 +200,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: 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: @@ -215,7 +231,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: 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() # 根据数据库类型选择列名格式 @@ -244,7 +260,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: 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: @@ -264,7 +280,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): with get_connection() as db: 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]]: """ @@ -280,7 +296,9 @@ class MaterialsToBeDeletedDAO(BaseDAO): """ 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. @@ -290,7 +308,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: 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() # 根据数据库类型选择列名格式 @@ -323,7 +341,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: True if successful, False otherwise """ - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -356,7 +374,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: Number of records deleted """ - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -385,7 +403,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: Number of records deleted """ - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") sql = f"DELETE FROM {table_name}" try: @@ -412,16 +430,18 @@ class MaterialsToBeDeletedDAO(BaseDAO): total_deleted = 0 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() - placeholders = ','.join([placeholder for _ in batch]) - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + placeholders = ",".join([placeholder for _ in batch]) + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") # 根据数据库类型选择列名格式 if self._db_type == DatabaseType.MYSQL: sql = f"DELETE FROM {table_name} WHERE MaterialCode IN ({placeholders})" else: - sql = f"DELETE FROM {table_name} WHERE [MaterialCode] IN ({placeholders})" + sql = ( + f"DELETE FROM {table_name} WHERE [MaterialCode] IN ({placeholders})" + ) try: with get_connection() as db: @@ -444,7 +464,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: True if material exists, False otherwise """ - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -463,7 +483,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): with get_connection() as db: 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: """ @@ -472,12 +492,12 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: 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}" with get_connection() as db: 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: """ @@ -489,7 +509,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Returns: Number of materials for the manager """ - table_name = self._convert_sql('[dbo].[MaterialsToBeDeleted]') + table_name = self._convert_sql("[dbo].[MaterialsToBeDeleted]") placeholder = self._get_placeholder() # 根据数据库类型选择列名格式 @@ -508,7 +528,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): with get_connection() as db: 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]: """ @@ -518,7 +538,7 @@ class MaterialsToBeDeletedDAO(BaseDAO): Dictionary with statistics including total materials, 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: @@ -560,8 +580,8 @@ class MaterialsToBeDeletedDAO(BaseDAO): # Get materials per manager manager_results = db.execute_query(manager_sql) - stats['materials_per_manager'] = [ - {r['ManagerName']: r['count']} for r in manager_results + stats["materials_per_manager"] = [ + {r["ManagerName"]: r["count"]} for r in manager_results ] return stats diff --git a/db/materials_to_delete.py b/db/materials_to_delete.py index c12dd0d..0e3fc7d 100644 --- a/db/materials_to_delete.py +++ b/db/materials_to_delete.py @@ -72,7 +72,9 @@ def get_materials_to_delete_by_managers( """ with get_connection() as conn: 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 else: # 使用 IN 子句查询多个负责人 @@ -85,7 +87,9 @@ def get_materials_to_delete_by_managers( """ with get_connection() as conn: 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 diff --git a/db/mysql_connection.py b/db/mysql_connection.py index 887cceb..0ca2e97 100644 --- a/db/mysql_connection.py +++ b/db/mysql_connection.py @@ -40,13 +40,13 @@ class MySQLConnection(BaseDatabaseConnection): try: self.connection = mysql.connector.connect( - host=self.config.get('host', 'localhost'), - port=self.config.get('port', 3306), - database=self.config['database'], - user=self.config['username'], - password=self.config['password'], - charset=self.config.get('charset', 'utf8mb4'), - autocommit=False + host=self.config.get("host", "localhost"), + port=self.config.get("port", 3306), + database=self.config["database"], + user=self.config["username"], + password=self.config["password"], + charset=self.config.get("charset", "utf8mb4"), + autocommit=False, ) print( f"成功连接到 MySQL 数据库: {self.config.get('host', 'localhost')}" @@ -153,7 +153,7 @@ class MySQLConnection(BaseDatabaseConnection): Returns: str: SQL query with MySQL-compatible placeholders """ - return sql.replace('?', '%s') + return sql.replace("?", "%s") def _convert_table_names(self, sql: str) -> str: """ @@ -171,10 +171,10 @@ class MySQLConnection(BaseDatabaseConnection): import re # 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) - sql = re.sub(r'\[([^\]]+)\]', r'\1', sql) + sql = re.sub(r"\[([^\]]+)\]", r"\1", sql) return sql diff --git a/db/production_contract_data_dao.py b/db/production_contract_data_dao.py index ac3f77d..1834fd4 100644 --- a/db/production_contract_data_dao.py +++ b/db/production_contract_data_dao.py @@ -32,12 +32,14 @@ class ProductionContractDataDAO(BaseDAO): all_results = [] 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() - 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: @@ -73,9 +75,9 @@ class ProductionContractDataDAO(BaseDAO): """ results = self.query_by_总排号(总排号_list) # Extract unique 生产订单号 values, excluding None/null values - source_numbers = list(set( - [r['生产订单号'] for r in results if r.get('生产订单号')] - )) + source_numbers = list( + set([r["生产订单号"] for r in results if r.get("生产订单号")]) + ) return source_numbers def get_生产订单号_map(self, 总排号_list: List[str]) -> Dict[str, str]: @@ -90,7 +92,7 @@ class ProductionContractDataDAO(BaseDAO): """ results = self.query_by_总排号(总排号_list) return { - r['总排号']: r['生产订单号'] + r["总排号"]: r["生产订单号"] for r in results - if r.get('总排号') and r.get('生产订单号') + if r.get("总排号") and r.get("生产订单号") } diff --git a/db/production_order_query.py b/db/production_order_query.py index 98355ea..089bcfb 100644 --- a/db/production_order_query.py +++ b/db/production_order_query.py @@ -95,7 +95,9 @@ def _query_order_numbers_from_db(production_ids, db_type): 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) return all_results @@ -118,7 +120,7 @@ def query_production_order_numbers(inputs): return [] production_ids = [] # 需要查询数据库的 - order_numbers = [] # 直接使用的 + order_numbers = [] # 直接使用的 for item in inputs: input_type = identify_input_type(item) diff --git a/db/sqlserver_connection.py b/db/sqlserver_connection.py index 0781cb7..4a09d29 100644 --- a/db/sqlserver_connection.py +++ b/db/sqlserver_connection.py @@ -38,7 +38,7 @@ class SQLServerConnection(BaseDatabaseConnection): 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 = ( f"DRIVER={{{driver}}};" f"SERVER={self.config['server']};" diff --git a/db/table_name_converter.py b/db/table_name_converter.py index 117ba02..b4f417b 100644 --- a/db/table_name_converter.py +++ b/db/table_name_converter.py @@ -12,7 +12,7 @@ class TableNameConverter: """表名转换工具类""" # 匹配 SQL Server 表名格式:[schema].[tablename] 或 [schema].[table name] - SQLSERVER_PATTERN = re.compile(r'\[([^\]]+)\]\.\[([^\]]+)\]') + SQLSERVER_PATTERN = re.compile(r"\[([^\]]+)\]\.\[([^\]]+)\]") @staticmethod def to_mysql(table_name: str) -> str: @@ -66,7 +66,7 @@ class TableNameConverter: '[productionContractData].[26年压力表合同数据]' """ # 分割第一个下划线 - parts = table_name.split('_', 1) + parts = table_name.split("_", 1) if len(parts) == 2: schema = parts[0] table = parts[1] @@ -92,21 +92,24 @@ class TableNameConverter: >>> TableNameConverter.convert_sql(sql, 'mysql') 'SELECT * FROM dbo_BIPUsers WHERE ID = ?' """ - if db_type == 'mysql': + if db_type == "mysql": # SQL Server → MySQL def replace_to_mysql(match): schema = match.group(1) table = match.group(2) return f"{schema}_{table}" + result = TableNameConverter.SQLSERVER_PATTERN.sub(replace_to_mysql, sql) return result - elif db_type == 'sqlserver': + elif db_type == "sqlserver": # MySQL → SQL Server # 首先查找可能的 MySQL 格式表名(schema_table 格式) # 这是一个简化版本,可能无法处理所有边缘情况 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) for schema, table in set(matches): mysql_name = f"{schema}_{table}" @@ -133,7 +136,7 @@ class TableNameConverter: tables.append(f"{schema}_{table}") # 查找可能的 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) tables.extend(mysql_matches) diff --git a/gui/__init__.py b/gui/__init__.py index f6c5b8d..7c527d7 100644 --- a/gui/__init__.py +++ b/gui/__init__.py @@ -19,13 +19,13 @@ from .constants import ( from .utils import admin_only, require_session __all__ = [ - 'BaseTab', - 'WINDOW_SIZE', - 'MIN_WINDOW_SIZE', - 'POLL_INTERVAL_MS', - 'LOG_COLORS', - 'DEFAULT_FONT_FAMILY', - 'DEFAULT_FONT_SIZE', - 'admin_only', - 'require_session', + "BaseTab", + "WINDOW_SIZE", + "MIN_WINDOW_SIZE", + "POLL_INTERVAL_MS", + "LOG_COLORS", + "DEFAULT_FONT_FAMILY", + "DEFAULT_FONT_SIZE", + "admin_only", + "require_session", ] diff --git a/gui/base_tab.py b/gui/base_tab.py index 7197967..8ce4cc0 100644 --- a/gui/base_tab.py +++ b/gui/base_tab.py @@ -48,7 +48,7 @@ class BaseTab(ttk.Frame): 如果 self.logger 未设置,将使用 logging.getLogger(__name__) 作为后备。 """ # 获取 logger(优先使用实例的 logger,否则使用模块 logger) - logger = getattr(self, 'logger', None) or logging.getLogger(__name__) + logger = getattr(self, "logger", None) or logging.getLogger(__name__) # 将自定义级别映射到 logging 级别 level_upper = level.upper() @@ -83,11 +83,11 @@ class BaseTab(ttk.Frame): 需要 main_window 或 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() # 尝试从 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 False @@ -101,10 +101,10 @@ class BaseTab(ttk.Frame): Note: 需要 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 "" - 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 "" @@ -128,5 +128,5 @@ class BaseTab(ttk.Frame): def reload_config(self): """重新加载配置(子类可覆盖此方法)""" - if self.config and hasattr(self.config, 'reload'): - self.config.reload() \ No newline at end of file + if self.config and hasattr(self.config, "reload"): + self.config.reload() diff --git a/gui/config_manager.py b/gui/config_manager.py index 1824d3b..7eaaf0d 100644 --- a/gui/config_manager.py +++ b/gui/config_manager.py @@ -6,6 +6,7 @@ 负责加载、保存和管理用户配置。 支持从环境变量和 .env 文件加载配置。 """ + import os from typing import TYPE_CHECKING from config.loader import ConfigLoader @@ -20,7 +21,9 @@ if TYPE_CHECKING: 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)) # 如果是字符串且目标字段是枚举类型,进行转换 - if isinstance(value, str) and hasattr(field_type, "__members__"): # 它是一个 Enum + if isinstance(value, str) and hasattr( + field_type, "__members__" + ): # 它是一个 Enum try: value = field_type(value) except ValueError: diff --git a/gui/constants.py b/gui/constants.py index c0245d5..2dfecf3 100644 --- a/gui/constants.py +++ b/gui/constants.py @@ -45,11 +45,11 @@ HEADING_STORE_DELAY_MS = 100 # 日志级别对应的颜色 LOG_COLORS = { - "INFO": "#000000", # 黑色 - "SUCCESS": "#008000", # 绿色 - "WARNING": "#FF8C00", # 橙色 - "ERROR": "#FF0000", # 红色 - "DEBUG": "#808080", # 灰色 + "INFO": "#000000", # 黑色 + "SUCCESS": "#008000", # 绿色 + "WARNING": "#FF8C00", # 橙色 + "ERROR": "#FF0000", # 红色 + "DEBUG": "#808080", # 灰色 } # ============================================================================ @@ -109,4 +109,4 @@ DEFAULT_DB_BATCH_SIZE = 2000 CHECKBOX_CHECKED = "☑" # 未选中状态 -CHECKBOX_UNCHECKED = "☐" \ No newline at end of file +CHECKBOX_UNCHECKED = "☐" diff --git a/gui/data_extraction_tab.py b/gui/data_extraction_tab.py index 02f53f4..6e3bbbb 100644 --- a/gui/data_extraction_tab.py +++ b/gui/data_extraction_tab.py @@ -70,10 +70,12 @@ class DataExtractionTab(BaseTab): input_group.pack(fill=tk.BOTH, expand=True) self.production_id_input = ProductionIdInput( 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.text_widget.bind("", self._on_production_ids_changed) + self.production_id_input.text_widget.bind( + "", self._on_production_ids_changed + ) def _create_right_panel(self, parent): 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.pack(fill=tk.X, pady=5) self.output_file_selector = FileSelector( - output_group, label_text="保存为:", file_type="file", + output_group, + label_text="保存为:", + file_type="file", file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")], 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.pack(fill=tk.X, pady=5) 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.pack(fill=tk.X, pady=5) self.progress_bar = ttk.Progressbar(progress_group, mode="determinate") 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) button_frame = ttk.Frame(parent) 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.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) def _create_log_panel(self, parent): @@ -125,10 +137,11 @@ class DataExtractionTab(BaseTab): # 设置 GUI 日志处理器,将 logging 输出桥接到 LogText 组件 self._gui_handler = GuiTextHandler(self.log_text) - self._gui_handler.setFormatter(logging.Formatter( - '%(asctime)s [%(levelname)s] %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - )) + self._gui_handler.setFormatter( + logging.Formatter( + "%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" + ) + ) self.logger.addHandler(self._gui_handler) def _apply_ui_config(self): @@ -136,7 +149,7 @@ class DataExtractionTab(BaseTab): font_family = self.config.get("ui.font_family", "Microsoft YaHei UI") font_size = self.config.get("ui.font_size", 10) self.production_id_input.apply_font(font_family, font_size) - if hasattr(self.log_text, 'apply_font'): + if hasattr(self.log_text, "apply_font"): self.log_text.apply_font(font_family, font_size) except Exception as e: self.logger.debug(f"应用 UI 配置失败: {e}") @@ -163,7 +176,9 @@ class DataExtractionTab(BaseTab): self.status_label.config(text="正在初始化...") self.log_text.clear() 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() @@ -174,20 +189,28 @@ class DataExtractionTab(BaseTab): def _extraction_worker(self, production_ids: list[str], output_file: str): import tempfile + temp_file = None 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 - 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( username=self.config.get("erp.username"), password=self.config.get("erp.password"), headless=self.headless_var.get(), verbose=self.config.get("extraction.verbose", True), 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 参数 @@ -196,11 +219,15 @@ class DataExtractionTab(BaseTab): level = progress_info.detail.get("log_level", "INFO").upper() self._update_log(progress_info.message, level) else: - percent = self.progress_calculator.calculate_overall_percent(progress_info) + percent = self.progress_calculator.calculate_overall_percent( + progress_info + ) self._update_progress(percent, progress_info.message) result = self.extractor.extract( - production_id_file=temp_file, output_file=output_file, progress_callback=progress_callback + production_id_file=temp_file, + output_file=output_file, + progress_callback=progress_callback, ) if result and self.extracting: @@ -246,8 +273,10 @@ class DataExtractionTab(BaseTab): def _on_production_ids_changed(self, event=None): 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): self._apply_ui_config() - self._on_production_ids_changed() \ No newline at end of file + self._on_production_ids_changed() diff --git a/gui/log_config.py b/gui/log_config.py index de05f9b..97d5730 100644 --- a/gui/log_config.py +++ b/gui/log_config.py @@ -4,11 +4,12 @@ GUI 日志配置模块 统一配置 GUI 应用和控制台的日志输出 """ + import logging # 日志格式配置 -LOG_FORMAT = '%(asctime)s [%(levelname)s] %(message)s' -DATE_FORMAT = '%Y-%m-%d %H:%M:%S' +LOG_FORMAT = "%(asctime)s [%(levelname)s] %(message)s" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" def setup_gui_logging(level=logging.INFO): @@ -25,7 +26,7 @@ def setup_gui_logging(level=logging.INFO): level=level, format=LOG_FORMAT, datefmt=DATE_FORMAT, - force=True # 确保重新配置(即使之前配置过) + force=True, # 确保重新配置(即使之前配置过) ) return logging.getLogger() diff --git a/gui/login_dialog.py b/gui/login_dialog.py index ffefa76..aba7887 100644 --- a/gui/login_dialog.py +++ b/gui/login_dialog.py @@ -1,6 +1,7 @@ """ Login Dialog - Modal dialog for user authentication """ + import socket import tkinter as tk from tkinter import ttk, messagebox @@ -62,7 +63,7 @@ class LoginDialog: self._create_widgets() # Bind Enter key to login button - self.dialog.bind('', lambda e: self._on_login()) + self.dialog.bind("", lambda e: self._on_login()) # Focus on username entry self.username_entry.focus_set() @@ -74,19 +75,15 @@ class LoginDialog: main_frame.pack(fill=tk.BOTH, expand=True) # Title - title_label = ttk.Label( - main_frame, - text="请登录", - font=('', 16, 'bold') - ) + title_label = ttk.Label(main_frame, text="请登录", font=("", 16, "bold")) title_label.pack(pady=(0, 10)) # Computer name display computer_name_label = ttk.Label( main_frame, text=f"当前计算机: {socket.gethostname()}", - font=('', 9), - foreground='gray' + font=("", 9), + foreground="gray", ) computer_name_label.pack(pady=(0, 15)) @@ -112,28 +109,19 @@ class LoginDialog: # Login button login_btn = ttk.Button( - button_frame, - text="登录", - command=self._on_login, - width=10 + button_frame, text="登录", command=self._on_login, width=10 ) login_btn.pack(side=tk.LEFT, padx=5) # Cancel button cancel_btn = ttk.Button( - button_frame, - text="取消", - command=self._on_cancel, - width=10 + button_frame, text="取消", command=self._on_cancel, width=10 ) cancel_btn.pack(side=tk.LEFT, padx=5) # Version info version_label = ttk.Label( - main_frame, - text="v1.0", - font=('', 8), - foreground='gray' + main_frame, text="v1.0", font=("", 8), foreground="gray" ) version_label.pack(side=tk.BOTTOM, pady=10) diff --git a/gui/main_window.py b/gui/main_window.py index 3ee21c3..525526e 100644 --- a/gui/main_window.py +++ b/gui/main_window.py @@ -36,7 +36,9 @@ class MainWindow: # 设置窗口属性(包含用户信息) 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") # 设置最小窗口大小 @@ -62,8 +64,8 @@ class MainWindow: """更新共享的 Production ID 列表""" self.shared_production_ids = production_ids # 通知物料校验标签页 Production ID 已更新 - if hasattr(self, 'validation_tab'): - if hasattr(self.validation_tab, 'on_production_ids_updated'): + if hasattr(self, "validation_tab"): + if hasattr(self.validation_tab, "on_production_ids_updated"): self.validation_tab.on_production_ids_updated(production_ids) def create_menu(self): @@ -92,11 +94,15 @@ class MainWindow: self.notebook.add(self.extraction_tab, text="数据提取") # 物料校验标签页(传入 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="物料校验") # 设置标签页(传入 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="设置") # 初始化:如果数据提取页面已有 Production ID,通知物料校验页面 @@ -105,7 +111,7 @@ class MainWindow: def _initialize_shared_production_ids(self): """初始化共享的 Production ID(从数据提取页面获取)""" 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() if 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']} 身份登录" 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.set(user_info_text) @@ -181,9 +189,9 @@ class MainWindow: self.config.reload() # 通知各个标签页重新加载配置 - if hasattr(self.extraction_tab, 'reload_config'): + if hasattr(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() # 更新状态栏 diff --git a/gui/material_type_management_dialog.py b/gui/material_type_management_dialog.py index 2cf93ad..cfa5202 100644 --- a/gui/material_type_management_dialog.py +++ b/gui/material_type_management_dialog.py @@ -62,34 +62,22 @@ class ResultDialog(tk.Toplevel): text="✓", font=("Arial", 48), fg="#22c55e", # 绿色 - bg="#f0fdf4" # 浅绿背景 + bg="#f0fdf4", # 浅绿背景 ) icon_label.pack(side=tk.LEFT, padx=(0, 20)) # 背景框 - icon_frame = tk.Frame( - content_frame, - bg="#f0fdf4", - width=80, - height=80 - ) + icon_frame = tk.Frame(content_frame, bg="#f0fdf4", width=80, height=80) icon_frame.place(x=0, y=0) icon_frame.pack_propagate(False) icon_label = tk.Label( - icon_frame, - text="✓", - font=("Arial", 48), - fg="#22c55e", - bg="#f0fdf4" + icon_frame, text="✓", font=("Arial", 48), fg="#22c55e", bg="#f0fdf4" ) icon_label.place(relx=0.5, rely=0.5, anchor="center") else: # 失败图标:红色叉叉 icon_frame = tk.Frame( - content_frame, - bg="#fef2f2", # 浅红背景 - width=80, - height=80 + content_frame, bg="#fef2f2", width=80, height=80 # 浅红背景 ) icon_frame.pack_propagate(False) icon_frame.pack(side=tk.LEFT, padx=(0, 20)) @@ -99,7 +87,7 @@ class ResultDialog(tk.Toplevel): text="✕", font=("Arial", 48), fg="#ef4444", # 红色 - bg="#fef2f2" + bg="#fef2f2", ) icon_label.place(relx=0.5, rely=0.5, anchor="center") @@ -109,7 +97,7 @@ class ResultDialog(tk.Toplevel): text=message, font=("Microsoft YaHei UI", 10), justify=tk.LEFT, - wraplength=280 + wraplength=280, ) 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.pack(fill=tk.X, pady=(10, 0)) - ttk.Button( - button_frame, - text="确定", - command=self.destroy, - width=10 - ).pack(side=tk.RIGHT) + ttk.Button(button_frame, text="确定", command=self.destroy, width=10).pack( + side=tk.RIGHT + ) # 等待窗口关闭 self.wait_window() @@ -260,7 +245,9 @@ class EditableTreeview(ttk.Treeview): new_value = self.edit_entry.get() 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 = self.edit_entry @@ -276,7 +263,9 @@ class EditableTreeview(ttk.Treeview): # 调用回调 try: 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: # 回调出错,清除状态 print(f"Error in on_edit_complete: {e}") @@ -287,7 +276,9 @@ class EditableTreeview(ttk.Treeview): return 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 = self.edit_entry @@ -361,9 +352,11 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 数据缓存 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.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] = [] @@ -387,12 +380,18 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 顶部:筛选区域 - 仅管理员可见 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)) 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)) self._create_table(table_frame) @@ -411,12 +410,16 @@ class MaterialTypeManagementDialog(tk.Toplevel): # Canvas和滚动条 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.bind( "", - 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") @@ -428,15 +431,20 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 鼠标滚轮支持 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("", _on_mousewheel) # 快捷按钮 button_frame = ttk.Frame(parent) 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._deselect_all_managers).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="全选", command=self._select_all_managers).pack( + 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): """创建数据表格""" @@ -447,7 +455,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): show="headings", selectmode="extended", 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_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( - yscrollcommand=scrollbar_y.set, - xscrollcommand=scrollbar_x.set + yscrollcommand=scrollbar_y.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.add_command(label="新增记录 (Insert)", command=self._add_new_row) - self.context_menu.add_command(label="编辑记录 (F2)", command=self._edit_selected_cell) + self.context_menu.add_command( + 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_command(label="删除记录 (Delete)", command=self._delete_selected_rows) + self.context_menu.add_command( + label="删除记录 (Delete)", command=self._delete_selected_rows + ) self.tree.bind("", self._show_context_menu) 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="删除 (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) + ttk.Button(parent, text="新增 (Insert)", command=self._add_new_row).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="") @@ -501,8 +522,12 @@ class MaterialTypeManagementDialog(tk.Toplevel): right_frame = ttk.Frame(parent) 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._close_dialog).pack(side=tk.LEFT, padx=5) + ttk.Button(right_frame, text="保存", command=self._save_changes).pack( + 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): """显示右键菜单""" @@ -525,6 +550,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 管理员:获取所有负责人 from db.materials_to_be_deleted_dao import MaterialsTypeToBeDeletedDAO + dao = MaterialsTypeToBeDeletedDAO() self.managers = dao.get_managers() @@ -539,7 +565,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): self.filter_frame, text="全选", 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) # 负责人复选框 @@ -554,7 +580,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): self.filter_frame, text=manager, 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) def _on_select_all_toggle(self): @@ -587,8 +613,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): def _get_selected_managers(self) -> List[str]: """获取选中的负责人列表""" return [ - manager for manager, var in self.manager_checkboxes.items() - if var.get() + manager for manager, var in self.manager_checkboxes.items() if var.get() ] def _load_data(self): @@ -603,7 +628,9 @@ class MaterialTypeManagementDialog(tk.Toplevel): # PERMISSION CHECK: 非管理员用户只加载自己的数据 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: self.original_data = dao.get_all_materials() @@ -653,8 +680,12 @@ class MaterialTypeManagementDialog(tk.Toplevel): else: # 获取未删除的数据 base_data = [ - r for r in self.original_data - if self.row_status.get(self._get_record_key(r), self.ROW_STATUS_UNCHANGED) != self.ROW_STATUS_DELETED + r + 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: if self.tree.exists(item_id): values = self.tree.item(item_id, "values") - new_records.append({ - 'MaterialName': values[0], - 'ManagerName': values[1] - }) + new_records.append( + {"MaterialName": values[0], "ManagerName": values[1]} + ) except: pass # 合并数据并筛选 all_data = base_data + new_records self.current_data = [ - r for r in all_data - if r.get('ManagerName') in selected_managers + r for r in all_data if r.get("ManagerName") in selected_managers ] self._refresh_tree() @@ -714,10 +743,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): item_id = self.tree.insert( "", tk.END, - values=( - record.get('MaterialName', ''), - record.get('ManagerName', '') - ) + values=(record.get("MaterialName", ""), record.get("ManagerName", "")), ) # 恢复行状态 @@ -725,9 +751,9 @@ class MaterialTypeManagementDialog(tk.Toplevel): if key in self.row_status: status = self.row_status[key] 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: - self.tree.item(item_id, tags=('modified',)) + self.tree.item(item_id, tags=("modified",)) # 恢复正在编辑的新增行 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) # 更新 key 映射 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.tree.item(new_item_id, tags=('new',)) + self.tree.item(new_item_id, tags=("new",)) else: # 空行,保持临时 key self.row_status[temp_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('modified', background='#fff4e6') # 浅黄色 + self.tree.tag_configure("new", background="#e6f7e6") # 浅绿色 + self.tree.tag_configure("modified", background="#fff4e6") # 浅黄色 # 恢复选中状态 for material, manager in selected_data: @@ -765,7 +793,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): 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) @@ -787,7 +815,9 @@ class MaterialTypeManagementDialog(tk.Toplevel): if not selection: return - if not messagebox.askyesno("确认", f"确定要删除选中的 {len(selection)} 条记录吗?"): + if not messagebox.askyesno( + "确认", f"确定要删除选中的 {len(selection)} 条记录吗?" + ): return for item in selection: @@ -795,11 +825,16 @@ class MaterialTypeManagementDialog(tk.Toplevel): material_name = values[0] 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}" # 如果是新增的行,直接移除 - 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] else: # 标记为删除 @@ -809,7 +844,9 @@ class MaterialTypeManagementDialog(tk.Toplevel): 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() @@ -825,7 +862,10 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 检查是否是新增的行 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: @@ -834,13 +874,18 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 检查是否已输入完整数据 if new_material and new_manager: # 输入完整,更新 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(): if other_item == item_id: continue 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("警告", "该记录已存在") self.tree.item(item_id, values=("", "")) self._update_status() @@ -864,7 +909,9 @@ class MaterialTypeManagementDialog(tk.Toplevel): self.tree.item(item_id, values=new_values) 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(): @@ -883,7 +930,10 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 更新行状态 # 检查是否是新增的行(查找临时 key) 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: # 新增行:从临时 key 更新为实际 key @@ -904,7 +954,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): 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() @@ -926,9 +976,11 @@ class MaterialTypeManagementDialog(tk.Toplevel): for key, status in self.row_status.items(): if status == self.ROW_STATUS_DELETED: # 解析 key - parts = key.split('|') + parts = key.split("|") 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: # 从表格中获取数据 @@ -943,7 +995,9 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 正常 key,在表格中查找匹配的行 for item in self.tree.get_children(): 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: item_to_find = item break @@ -951,7 +1005,9 @@ class MaterialTypeManagementDialog(tk.Toplevel): if item_to_find: values = self.tree.item(item_to_find, "values") 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: # 从表格中获取新数据,从 original_values 获取旧数据 @@ -961,12 +1017,22 @@ class MaterialTypeManagementDialog(tk.Toplevel): # 从表格中找到对应的新数据 for item in self.tree.get_children(): 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: - to_update.append({ - 'old': {'MaterialName': old_material, 'ManagerName': old_manager}, - 'new': {'MaterialName': values[0], 'ManagerName': values[1]} - }) + to_update.append( + { + "old": { + "MaterialName": old_material, + "ManagerName": old_manager, + }, + "new": { + "MaterialName": values[0], + "ManagerName": values[1], + }, + } + ) break 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)} 条") if not messagebox.askyesno( - "确认保存", - "确定要将以下更改保存到数据库吗?\n\n" + "\n".join(msg_parts) + "确认保存", "确定要将以下更改保存到数据库吗?\n\n" + "\n".join(msg_parts) ): return @@ -996,60 +1061,62 @@ class MaterialTypeManagementDialog(tk.Toplevel): dao = MaterialsTypeToBeDeletedDAO() stats = { - 'insert_success': 0, - 'insert_failed': 0, - 'delete_success': 0, - 'delete_failed': 0, - 'update_success': 0, - 'update_failed': 0 + "insert_success": 0, + "insert_failed": 0, + "delete_success": 0, + "delete_failed": 0, + "update_success": 0, + "update_failed": 0, } # 执行插入 for record in to_insert: - if dao.insert_material(record['MaterialName'], record['ManagerName']): - stats['insert_success'] += 1 + if dao.insert_material(record["MaterialName"], record["ManagerName"]): + stats["insert_success"] += 1 else: - stats['insert_failed'] += 1 + stats["insert_failed"] += 1 # 执行删除 for record in to_delete: - if dao.delete_material(record['MaterialName'], record['ManagerName']): - stats['delete_success'] += 1 + if dao.delete_material(record["MaterialName"], record["ManagerName"]): + stats["delete_success"] += 1 else: - stats['delete_failed'] += 1 + stats["delete_failed"] += 1 # 执行更新 for update in to_update: - old = update['old'] - new = update['new'] - if dao.delete_material(old['MaterialName'], old['ManagerName']): - if dao.insert_material(new['MaterialName'], new['ManagerName']): - stats['update_success'] += 1 + old = update["old"] + new = update["new"] + if dao.delete_material(old["MaterialName"], old["ManagerName"]): + if dao.insert_material(new["MaterialName"], new["ManagerName"]): + stats["update_success"] += 1 else: - dao.insert_material(old['MaterialName'], old['ManagerName']) - stats['update_failed'] += 1 + dao.insert_material(old["MaterialName"], old["ManagerName"]) + stats["update_failed"] += 1 else: - stats['update_failed'] += 1 + stats["update_failed"] += 1 # 显示结果 result_parts = [] - if stats['insert_success'] > 0: + if stats["insert_success"] > 0: result_parts.append(f"新增成功:{stats['insert_success']} 条") - if stats['insert_failed'] > 0: + if stats["insert_failed"] > 0: result_parts.append(f"新增失败:{stats['insert_failed']} 条") - if stats['delete_success'] > 0: + if stats["delete_success"] > 0: result_parts.append(f"删除成功:{stats['delete_success']} 条") - if stats['delete_failed'] > 0: + if stats["delete_failed"] > 0: result_parts.append(f"删除失败:{stats['delete_failed']} 条") - if stats['update_success'] > 0: + if stats["update_success"] > 0: 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_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: ResultDialog(self, "保存完成(部分失败)", result_msg, success=False) else: @@ -1084,8 +1151,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): if total_changes > 0: self.status_label.config( - text=f"有 {total_changes} 项待保存的更改", - foreground="red" + text=f"有 {total_changes} 项待保存的更改", foreground="red" ) else: self.status_label.config(text="") @@ -1113,8 +1179,7 @@ class MaterialTypeManagementDialog(tk.Toplevel): if total_changes > 0: if not messagebox.askyesno( - "警告", - f"有 {total_changes} 项未保存的更改,确定要关闭吗?" + "警告", f"有 {total_changes} 项未保存的更改,确定要关闭吗?" ): return diff --git a/gui/material_validation_tab.py b/gui/material_validation_tab.py index 4a510b0..548dc61 100644 --- a/gui/material_validation_tab.py +++ b/gui/material_validation_tab.py @@ -19,7 +19,13 @@ from io import StringIO from contextlib import redirect_stdout from typing import List, Dict 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.log_config import setup_gui_logging, get_logger from gui.material_type_management_dialog import MaterialTypeManagementDialog @@ -31,7 +37,9 @@ import tempfile 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.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] = [] # 保存筛选状态 # 初始化统一日志系统 @@ -83,7 +93,7 @@ class MaterialValidationTab(BaseTab): manager_filter_frame = ttk.LabelFrame( main_container, text="筛选(按负责人)", - padding=(10, 10, 10, 10) # 左、上、右、下 - 底部padding减小 + padding=(10, 10, 10, 10), # 左、上、右、下 - 底部padding减小 ) manager_filter_frame.pack(fill=tk.X, pady=(0, 10)) else: @@ -135,7 +145,9 @@ class MaterialValidationTab(BaseTab): # 右侧:Production ID 数据源选择区域 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 数据源选择 self.production_id_source_var = tk.StringVar(value="shared") @@ -144,7 +156,7 @@ class MaterialValidationTab(BaseTab): text="使用文件:", variable=self.production_id_source_var, value="file", - command=self._on_production_id_source_changed + command=self._on_production_id_source_changed, ).pack(side=tk.LEFT) ttk.Radiobutton( @@ -152,7 +164,7 @@ class MaterialValidationTab(BaseTab): text="使用数据提取页面的 Production ID:", variable=self.production_id_source_var, value="shared", - command=self._on_production_id_source_changed + command=self._on_production_id_source_changed, ).pack(side=tk.LEFT, padx=10) # 文件选择器(放在 source_group 下方,当选择文件时显示) @@ -166,9 +178,7 @@ class MaterialValidationTab(BaseTab): # 共享 Production ID 提示标签 self.shared_ids_info_label = ttk.Label( - source_group, - text="", - foreground="blue" + source_group, text="", foreground="blue" ) # 初始时不显示 @@ -192,7 +202,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="全选", command=lambda: self._select_all(check=True), - state=tk.DISABLED + state=tk.DISABLED, ) self.select_all_button.pack(side=tk.LEFT, padx=5) @@ -201,7 +211,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="取消全选", command=lambda: self._select_all(check=False), - state=tk.DISABLED + state=tk.DISABLED, ) self.deselect_all_button.pack(side=tk.LEFT, padx=5) @@ -210,7 +220,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="确认删除", command=self.confirm_deletion, - state=tk.DISABLED + state=tk.DISABLED, ) self.confirm_delete_button.pack(side=tk.LEFT, padx=5) @@ -219,7 +229,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="执行删除", command=self.start_delete_execution, - state=tk.DISABLED + state=tk.DISABLED, ) self.execute_delete_button.pack(side=tk.LEFT, padx=5) @@ -227,9 +237,7 @@ class MaterialValidationTab(BaseTab): if self.session_manager.is_admin(): self.dryrun_var = tk.BooleanVar(value=False) self.dryrun_checkbox = ttk.Checkbutton( - button_frame, - text="预览模式 (不保存)", - variable=self.dryrun_var + button_frame, text="预览模式 (不保存)", variable=self.dryrun_var ) self.dryrun_checkbox.pack(side=tk.LEFT, padx=5) @@ -243,9 +251,7 @@ class MaterialValidationTab(BaseTab): # 类型管理按钮 self.type_management_button = ttk.Button( - button_frame, - text="类型管理", - command=self.open_type_management + button_frame, text="类型管理", command=self.open_type_management ) self.type_management_button.pack(side=tk.LEFT, padx=5) @@ -275,7 +281,7 @@ class MaterialValidationTab(BaseTab): columns=columns, show="headings", 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) # 添加滚动条 - scrollbar_y = ttk.Scrollbar(table_container, orient=tk.VERTICAL, command=self.tree.yview) - scrollbar_x = ttk.Scrollbar(table_container, orient=tk.HORIZONTAL, command=self.tree.xview) + scrollbar_y = ttk.Scrollbar( + 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( yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set @@ -319,7 +329,7 @@ class MaterialValidationTab(BaseTab): columns=columns, show="headings", 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) # 添加滚动条 - 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 ) @@ -367,7 +379,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="隐藏勾选", command=self._hide_checked_items, - state=tk.DISABLED + state=tk.DISABLED, ) self.btn_hide_checked.pack(fill=tk.X, pady=2) @@ -375,19 +387,19 @@ class MaterialValidationTab(BaseTab): button_frame, text="显示全部", command=self._show_all_items, - state=tk.DISABLED + state=tk.DISABLED, ) 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( button_frame, text="全选", command=lambda: self._select_all(check=True), - state=tk.DISABLED + state=tk.DISABLED, ) self.select_all_button.pack(fill=tk.X, pady=2) @@ -396,7 +408,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="取消全选", command=lambda: self._select_all(check=False), - state=tk.DISABLED + state=tk.DISABLED, ) self.deselect_all_button.pack(fill=tk.X, pady=2) @@ -405,7 +417,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="确认删除", command=self.confirm_deletion, - state=tk.DISABLED + state=tk.DISABLED, ) self.confirm_delete_button.pack(fill=tk.X, pady=2) @@ -414,7 +426,7 @@ class MaterialValidationTab(BaseTab): button_frame, text="执行删除", command=self.start_delete_execution, - state=tk.DISABLED + state=tk.DISABLED, ) self.execute_delete_button.pack(fill=tk.X, pady=2) @@ -439,7 +451,7 @@ class MaterialValidationTab(BaseTab): return 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) self.hidden_items.clear() @@ -452,10 +464,11 @@ class MaterialValidationTab(BaseTab): # 设置 GUI 日志处理器,将 logging 输出桥接到 LogText 组件 self._gui_handler = GuiTextHandler(self.log_text) - self._gui_handler.setFormatter(logging.Formatter( - '%(asctime)s [%(levelname)s] %(message)s', - datefmt='%Y-%m-%d %H:%M:%S' - )) + self._gui_handler.setFormatter( + logging.Formatter( + "%(asctime)s [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S" + ) + ) self.logger.addHandler(self._gui_handler) def _on_source_mode_change(self): @@ -468,16 +481,18 @@ class MaterialValidationTab(BaseTab): if mode == "database_full": # 全表模式:隐藏所有 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() - if hasattr(self, 'db_filtered_production_id_selector'): + if hasattr(self, "db_filtered_production_id_selector"): 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() elif mode == "database_filtered": # 过滤模式:显示数据源选择 - if hasattr(self, 'source_frame') and self.source_frame: - self.source_frame.grid(row=0, column=1, rowspan=2, sticky="nsew", padx=(20, 0)) + if hasattr(self, "source_frame") and self.source_frame: + self.source_frame.grid( + row=0, column=1, rowspan=2, sticky="nsew", padx=(20, 0) + ) # 根据 Production ID 数据源选择显示对应控件 self._on_production_id_source_changed() @@ -491,11 +506,15 @@ class MaterialValidationTab(BaseTab): if source == "shared": # 使用共享 Production ID 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: # 使用文件 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): """当数据提取页面的 Production ID 更新时调用""" @@ -506,7 +525,7 @@ class MaterialValidationTab(BaseTab): # User 模式:静默更新,不显示任何提示 if is_user_only: # 启用执行删除按钮(如果有 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) return @@ -518,19 +537,22 @@ class MaterialValidationTab(BaseTab): preview += f" ... (共 {count} 个)" self.shared_ids_info_label.config(text=f"📋 {preview}") # 显示提示标签 - if hasattr(self, 'shared_ids_label') and not self.shared_ids_label.winfo_ismapped(): + if ( + hasattr(self, "shared_ids_label") + and not self.shared_ids_label.winfo_ismapped() + ): self.shared_ids_label.pack(anchor="w", pady=(0, 5)) # 启用执行删除按钮 - if hasattr(self, 'execute_delete_button'): + if hasattr(self, "execute_delete_button"): self.execute_delete_button.config(state=tk.NORMAL) else: self.shared_ids_info_label.config(text="") # 隐藏提示标签 - if hasattr(self, 'shared_ids_label'): + if hasattr(self, "shared_ids_label"): if self.shared_ids_label.winfo_ismapped(): 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) def reload_config(self): @@ -557,8 +579,12 @@ class MaterialValidationTab(BaseTab): button_frame = ttk.Frame(main_container) 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(button_frame, text="取消全选", command=self._deselect_all_managers_button).pack(fill=tk.X, pady=2) + ttk.Button( + 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) self.filter_frame = ttk.Frame(main_container) @@ -579,7 +605,7 @@ class MaterialValidationTab(BaseTab): # 不显示复选框,直接显示提示信息 ttk.Label( self.filter_frame, - text=f"仅显示您的数据(负责人:{self.session_manager.get_username()})" + text=f"仅显示您的数据(负责人:{self.session_manager.get_username()})", ).pack(anchor="w") return @@ -613,7 +639,7 @@ class MaterialValidationTab(BaseTab): self.filter_frame, text=manager, 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) def _on_select_all_managers_toggle(self): @@ -654,8 +680,7 @@ class MaterialValidationTab(BaseTab): # 管理员:从复选框获取选中的负责人 return [ - manager for manager, var in self.manager_checkboxes.items() - if var.get() + manager for manager, var in self.manager_checkboxes.items() if var.get() ] def _apply_manager_filter(self): @@ -677,14 +702,19 @@ class MaterialValidationTab(BaseTab): # 筛选记录:包含选中负责人的记录 + 负责人为空的记录 filtered_records = [ - record for record in self.material_records_cache - if (record.manager_name in selected_managers or - not record.manager_name or record.manager_name.strip() == "") + record + for record in self.material_records_cache + if ( + record.manager_name in selected_managers + or not record.manager_name + or record.manager_name.strip() == "" + ) ] # 统计空负责人的记录数 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() == "" ) @@ -692,7 +722,7 @@ class MaterialValidationTab(BaseTab): self._update_log( f"筛选结果:共 {len(filtered_records)} 条记录" f"(其中 {empty_manager_count} 条负责人为空,待编辑)", - "INFO" + "INFO", ) def _refresh_filtered_results(self, filtered_records): @@ -706,19 +736,22 @@ class MaterialValidationTab(BaseTab): # 从数据库获取已标记删除的记录 from db.materials_to_be_deleted_records_dao import MaterialsToBeDeletedDAO + record_dao = MaterialsToBeDeletedDAO() # PERMISSION CHECK: 非管理员用户只获取自己的记录 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: marked_records = record_dao.get_all_records() # Build dictionary: MaterialCode -> ManagerName marked_codes_dict = { - r['MaterialCode']: r['ManagerName'] + r["MaterialCode"]: r["ManagerName"] 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( - "编辑负责人", - f"请输入负责人姓名:", - initialvalue=current_value + "编辑负责人", f"请输入负责人姓名:", initialvalue=current_value ) if new_value is not None: # 用户没有取消 @@ -853,9 +884,14 @@ class MaterialValidationTab(BaseTab): # User 模式:自动使用共享的 Production ID production_ids_list = self.shared_production_ids if not production_ids_list: - messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试") + messagebox.showerror( + "错误", + "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试", + ) return - self.log_text.info(f"使用数据提取页面的 Production ID({len(production_ids_list)} 个)") + self.log_text.info( + f"使用数据提取页面的 Production ID({len(production_ids_list)} 个)" + ) else: # Admin 模式:检查数据源选择 source = self.production_id_source_var.get() @@ -863,9 +899,14 @@ class MaterialValidationTab(BaseTab): # 使用共享的 Production ID production_ids_list = self.shared_production_ids if not production_ids_list: - messagebox.showerror("错误", "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试") + messagebox.showerror( + "错误", + "没有可用的共享 Production ID\n请在数据提取页面输入 Production ID 后再试", + ) return - self.log_text.info(f"使用共享的 Production ID({len(production_ids_list)} 个)") + self.log_text.info( + f"使用共享的 Production ID({len(production_ids_list)} 个)" + ) else: # 使用文件 production_id_file = self.db_filtered_production_id_selector.get() @@ -873,7 +914,9 @@ class MaterialValidationTab(BaseTab): messagebox.showerror("错误", "请选择 ProductionID 文件") return if not os.path.exists(production_id_file): - messagebox.showerror("错误", f"文件不存在:{production_id_file}") + messagebox.showerror( + "错误", f"文件不存在:{production_id_file}" + ) return else: messagebox.showerror("错误", f"未知的校验模式: {mode}") @@ -897,9 +940,9 @@ class MaterialValidationTab(BaseTab): # 禁用筛选按钮(User 模式) 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) - if hasattr(self, 'btn_show_all'): + if hasattr(self, "btn_show_all"): self.btn_show_all.config(state=tk.DISABLED) # 保存当前筛选状态 @@ -914,26 +957,43 @@ class MaterialValidationTab(BaseTab): # 在后台线程中执行校验 validation_thread = threading.Thread( 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, ) validation_thread.start() 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 + temp_production_id_file = None try: # 如果提供了共享的 Production ID 列表,创建临时文件 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 - f.write('\n'.join(production_ids_list)) + f.write("\n".join(production_ids_list)) production_id_file = temp_production_id_file - self._update_log(f"使用共享的 Production ID 列表({len(production_ids_list)} 个)", "INFO") + self._update_log( + f"使用共享的 Production ID 列表({len(production_ids_list)} 个)", + "INFO", + ) # 导入校验器 from utils.material_status_validator import MaterialStatusValidator @@ -953,14 +1013,13 @@ class MaterialValidationTab(BaseTab): with redirect_stdout(captured_output): if mode == "database_full": result_file, results = validator.validate_from_database_enhanced( - full_table=True, - output_file=output_file + full_table=True, output_file=output_file ) elif mode == "database_filtered": result_file, results = validator.validate_from_database_enhanced( production_id_file=production_id_file, full_table=False, - output_file=output_file + output_file=output_file, ) else: raise ValueError(f"未知的校验模式: {mode}") @@ -981,13 +1040,16 @@ class MaterialValidationTab(BaseTab): self._load_results_with_deletion_status(result_file) elif results is not None and len(results) == 0: # 已经在 validator 中输出详细错误信息,这里只做简单提示 - self._update_log("校验失败:未找到物料记录,请查看上方日志了解详细原因", "ERROR") + self._update_log( + "校验失败:未找到物料记录,请查看上方日志了解详细原因", "ERROR" + ) else: self._update_log("校验失败", "ERROR") except Exception as e: self._update_log(f"校验过程中发生错误:{str(e)}", "ERROR") import traceback + self._update_log(traceback.format_exc(), "ERROR") finally: # 清理临时文件 @@ -1009,21 +1071,24 @@ class MaterialValidationTab(BaseTab): # 启用执行删除按钮(需要 Production ID) if self.shared_production_ids or ( - self.session_manager.is_admin() and - hasattr(self, 'db_filtered_production_id_selector') and - self.db_filtered_production_id_selector.get() + self.session_manager.is_admin() + and hasattr(self, "db_filtered_production_id_selector") + and self.db_filtered_production_id_selector.get() ): self.execute_delete_button.config(state=tk.NORMAL) # 启用筛选按钮(User 模式) 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) - if hasattr(self, 'btn_show_all'): + if hasattr(self, "btn_show_all"): 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) def _load_results_with_deletion_status(self, file_path: str): @@ -1034,20 +1099,25 @@ class MaterialValidationTab(BaseTab): # 在主线程中更新表格 def update_table(): # 从数据库获取已标记删除的 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() # PERMISSION CHECK: 非管理员用户只获取自己的记录 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: marked_records = record_dao.get_all_records() # Build dictionary: MaterialCode -> ManagerName marked_codes_dict = { - r['MaterialCode']: r['ManagerName'] + r["MaterialCode"]: r["ManagerName"] 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(): @@ -1080,7 +1150,10 @@ class MaterialValidationTab(BaseTab): if len(df) > 0: 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() @@ -1129,8 +1202,8 @@ class MaterialValidationTab(BaseTab): return # 收集所有数据 - to_upsert = [] # 需要写入/更新的记录 - to_delete = [] # 需要删除的记录 + to_upsert = [] # 需要写入/更新的记录 + to_delete = [] # 需要删除的记录 missing_manager = [] # 缺少负责人的记录 for item in self.tree.get_children(): @@ -1150,18 +1223,21 @@ class MaterialValidationTab(BaseTab): if not manager_name or not manager_name.strip(): missing_manager.append(material_code) else: - to_upsert.append({ - "material_code": material_code, - "manager_name": manager_name.strip() - }) + to_upsert.append( + { + "material_code": material_code, + "manager_name": manager_name.strip(), + } + ) else: # 未勾选:需要删除 to_delete.append(material_code) # 验证:已勾选的记录必须有负责人 if missing_manager: - msg = f"以下已勾选的记录缺少负责人信息,无法保存:\n\n" + \ - "\n".join(missing_manager[:10]) + msg = f"以下已勾选的记录缺少负责人信息,无法保存:\n\n" + "\n".join( + missing_manager[:10] + ) if len(missing_manager) > 10: msg += f"\n... 共 {len(missing_manager)} 条" messagebox.showwarning("警告", msg) @@ -1183,7 +1259,10 @@ class MaterialValidationTab(BaseTab): 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( target=self._execute_sync_in_background, @@ -1205,7 +1284,7 @@ class MaterialValidationTab(BaseTab): dao = MaterialsToBeDeletedDAO() # 执行写入/更新操作 - upsert_stats = {'total': 0, 'success': 0, 'failed': 0} + upsert_stats = {"total": 0, "success": 0, "failed": 0} if to_upsert: upsert_stats = dao.upsert_batch(to_upsert) @@ -1216,9 +1295,9 @@ class MaterialValidationTab(BaseTab): # 组合统计信息 combined_stats = { - 'upsert_success': upsert_stats['success'], - 'upsert_failed': upsert_stats['failed'], - 'deleted': delete_count + "upsert_success": upsert_stats["success"], + "upsert_failed": upsert_stats["failed"], + "deleted": delete_count, } self.after(0, lambda: self._sync_complete(combined_stats)) @@ -1232,9 +1311,9 @@ class MaterialValidationTab(BaseTab): Args: stats: 包含 upsert_success, upsert_failed, deleted 的字典 """ - upsert_success = stats.get('upsert_success', 0) - upsert_failed = stats.get('upsert_failed', 0) - deleted = stats.get('deleted', 0) + upsert_success = stats.get("upsert_success", 0) + upsert_failed = stats.get("upsert_failed", 0) + deleted = stats.get("deleted", 0) msg_parts = [] if upsert_success > 0: @@ -1260,14 +1339,15 @@ class MaterialValidationTab(BaseTab): """导出结果到 Excel""" # 从配置获取输出文件路径 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) # 如果配置的文件不存在,提示用户选择位置 if not os.path.exists(data_dir): output_dir = filedialog.askdirectory( - title="选择输出目录", - initialdir=data_dir + title="选择输出目录", initialdir=data_dir ) if output_dir: self.config.set("paths.data_dir", output_dir) @@ -1279,7 +1359,9 @@ class MaterialValidationTab(BaseTab): # 如果文件已存在,提示用户覆盖 if os.path.exists(output_file): - if not messagebox.askyesno("确认覆盖", f"文件已存在:{output_file}\n是否覆盖?"): + if not messagebox.askyesno( + "确认覆盖", f"文件已存在:{output_file}\n是否覆盖?" + ): return try: @@ -1291,14 +1373,16 @@ class MaterialValidationTab(BaseTab): checkbox = values[0] if values else "☐" is_checked = checkbox == "☑" - data.append([ - "是" if is_checked else "否", # 选择状态 - values[1] if len(values) > 1 else "", # 材料名称 - values[2] if len(values) > 2 else "", # 材料代码 - values[3] if len(values) > 3 else "", # 规格 - values[4] if len(values) > 4 else "", # 型号 - values[5] if len(values) > 5 else "", # 负责人 - ]) + data.append( + [ + "是" if is_checked else "否", # 选择状态 + values[1] if len(values) > 1 else "", # 材料名称 + values[2] if len(values) > 2 else "", # 材料代码 + values[3] if len(values) > 3 else "", # 规格 + values[4] if len(values) > 4 else "", # 型号 + values[5] if len(values) > 5 else "", # 负责人 + ] + ) if not data: messagebox.showwarning("警告", "没有数据可导出") @@ -1319,7 +1403,9 @@ class MaterialValidationTab(BaseTab): def open_type_management(self): """打开类型管理窗口""" - dialog = MaterialTypeManagementDialog(self, self.session_manager, title="物料类型管理") + dialog = MaterialTypeManagementDialog( + self, self.session_manager, title="物料类型管理" + ) def start_delete_execution(self): """开始执行删除""" @@ -1329,7 +1415,7 @@ class MaterialValidationTab(BaseTab): if is_admin: # Admin 用户:检查数据源选择 - if hasattr(self, 'production_id_source_var'): + if hasattr(self, "production_id_source_var"): source = self.production_id_source_var.get() if source == "shared": production_ids = self.shared_production_ids @@ -1337,6 +1423,7 @@ class MaterialValidationTab(BaseTab): production_id_file = self.db_filtered_production_id_selector.get() if production_id_file and os.path.exists(production_id_file): from db.production_order_query import read_production_ids + production_ids = read_production_ids(production_id_file) else: # 普通用户:使用共享的 Production ID @@ -1344,7 +1431,9 @@ class MaterialValidationTab(BaseTab): # 验证 Production ID if not production_ids: - messagebox.showerror("错误", "没有可用的 Production ID\n请先在校验页面获取数据") + messagebox.showerror( + "错误", "没有可用的 Production ID\n请先在校验页面获取数据" + ) return # 2. 获取负责人 @@ -1358,7 +1447,7 @@ class MaterialValidationTab(BaseTab): # 3. 获取 dryrun 设置 dryrun = False - if is_admin and hasattr(self, 'dryrun_var'): + if is_admin and hasattr(self, "dryrun_var"): # Admin 用户:使用界面上的 dryrun 复选框 dryrun = self.dryrun_var.get() else: @@ -1384,7 +1473,7 @@ class MaterialValidationTab(BaseTab): title="执行删除", managers=manager_text, dryrun=dryrun, - on_cancel=self._cancel_delete_execution + on_cancel=self._cancel_delete_execution, ) self.log_text.info(f"开始执行删除(模式: {'预览' if dryrun else '正式'})...") @@ -1393,14 +1482,14 @@ class MaterialValidationTab(BaseTab): delete_thread = threading.Thread( target=self._delete_worker, args=(production_ids, manager_names, dryrun), - daemon=True + daemon=True, ) delete_thread.start() def _cancel_delete_execution(self): """取消删除执行""" 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") 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 # 创建临时文件保存 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 - f.write('\n'.join(production_ids)) + f.write("\n".join(production_ids)) # 进度回调函数 def progress_callback(current: int, total: int, message: str): @@ -1439,7 +1530,7 @@ class MaterialValidationTab(BaseTab): headless=self.config.get("erp.headless", True), verbose=True, 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.set_completed() import traceback + self.after(0, lambda: self._update_log(traceback.format_exc(), "ERROR")) finally: # 清理临时文件 try: - if 'temp_file' in locals() and os.path.exists(temp_file): + if "temp_file" in locals() and os.path.exists(temp_file): os.unlink(temp_file) except OSError as e: self.logger.debug(f"清理临时文件失败: {e}") @@ -1484,11 +1576,13 @@ class MaterialValidationTab(BaseTab): """ # 更新日志 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('skipped_materials', []))} 条") 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) diff --git a/gui/progress.py b/gui/progress.py index f7763bf..4b89114 100644 --- a/gui/progress.py +++ b/gui/progress.py @@ -18,9 +18,7 @@ class ProgressInfo: 用于在后台任务和 GUI 之间传递进度信息。 """ - stage: ( - str # 阶段标识: 'login', 'query', 'download', 'logout', 'convert', 'database', 'complete' - ) + stage: str # 阶段标识: 'login', 'query', 'download', 'logout', 'convert', 'database', 'complete' current: int # 当前进度值 total: int # 总量 message: str # 显示给用户的消息 diff --git a/gui/settings_tab.py b/gui/settings_tab.py index 866a1c7..d073f44 100644 --- a/gui/settings_tab.py +++ b/gui/settings_tab.py @@ -38,7 +38,9 @@ class SettingsTab(ttk.Frame): 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) @@ -158,7 +160,9 @@ class SettingsTab(ttk.Frame): self.sqlserver_frame = ttk.Frame(group) 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() ttk.Entry(self.sqlserver_frame, textvariable=self.db_server_var, width=50).grid( row=0, column=1, pady=5, sticky="ew" @@ -167,16 +171,24 @@ class SettingsTab(ttk.Frame): # MySQL 配置 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() ttk.Entry(self.mysql_frame, textvariable=self.mysql_host_var, width=50).grid( 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) 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) # 通用配置(两种数据库都需要) @@ -207,7 +219,9 @@ class SettingsTab(ttk.Frame): self.mysql_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5) else: 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): """创建浏览器配置组""" @@ -232,7 +246,9 @@ class SettingsTab(ttk.Frame): def _create_paths_group(self, parent): """创建路径配置组""" # 根据用户类型调整 grid 位置 - is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User' + is_user_only = ( + self.session_manager and self.session_manager.get_user_type() == "User" + ) group = ttk.LabelFrame(parent, text="路径设置", padding=10) 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.Entry(group, textvariable=self.validation_output_filename_var, width=40).grid( - row=3, column=1, columnspan=2, sticky="ew", pady=5 - ) + ttk.Entry( + group, textvariable=self.validation_output_filename_var, width=40 + ).grid(row=3, column=1, columnspan=2, sticky="ew", pady=5) group.columnconfigure(0, weight=1) @@ -275,16 +291,14 @@ class SettingsTab(ttk.Frame): # dryrun 模式设置 self.user_dryrun_var = tk.BooleanVar(value=False) ttk.Checkbutton( - group, - text="预览模式 (执行删除时不保存更改)", - variable=self.user_dryrun_var + group, text="预览模式 (执行删除时不保存更改)", variable=self.user_dryrun_var ).grid(row=0, column=0, sticky="w", pady=5) # 说明文字 hint_label = ttk.Label( group, text="提示:勾选后,执行删除操作时将只预览不实际保存,用于测试流程。", - foreground="gray" + foreground="gray", ) 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( group, 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", width=30, ) @@ -344,20 +363,28 @@ class SettingsTab(ttk.Frame): # 使用数据库 self.validation_use_database_var = tk.BooleanVar() 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) # 输出文件名 ttk.Label(group, text="输出文件名:").grid(row=2, column=0, sticky="w", pady=5) - ttk.Entry(group, textvariable=self.validation_output_filename_var, width=30).grid( - row=2, column=1, sticky="w", pady=5 - ) + ttk.Entry( + group, textvariable=self.validation_output_filename_var, width=30 + ).grid(row=2, column=1, sticky="w", pady=5) # 批次大小 - ttk.Label(group, text="数据库批次大小:").grid(row=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) 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) # 匹配模式 @@ -375,15 +402,17 @@ class SettingsTab(ttk.Frame): # CRUD 操作 self.validation_enable_crud_var = tk.BooleanVar() 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) # 默认负责人 ttk.Label(group, text="默认负责人:").grid(row=6, column=0, sticky="w", pady=5) self.validation_default_manager_var = tk.StringVar() - ttk.Entry(group, textvariable=self.validation_default_manager_var, width=30).grid( - row=6, column=1, sticky="w", pady=5 - ) + ttk.Entry( + group, textvariable=self.validation_default_manager_var, width=30 + ).grid(row=6, column=1, sticky="w", pady=5) group.columnconfigure(1, weight=1) @@ -398,7 +427,14 @@ class SettingsTab(ttk.Frame): font_combo = ttk.Combobox( group, 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", width=30, ) @@ -412,7 +448,9 @@ class SettingsTab(ttk.Frame): ).grid(row=1, column=1, sticky="w", pady=5) # 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) ttk.Spinbox( 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): """从配置加载设置到界面""" # 判断是否为仅测试用户模式 - 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: # User 用户模式 - 只需要加载路径设置到界面 # 路径设置 self.data_dir_selector.set(self.config.get("paths.data_dir", "")) self.default_output_var.set(self.config.get("paths.default_output", "")) - self.validation_output_filename_var.set(self.config.get("paths.validation_output", "")) + self.validation_output_filename_var.set( + self.config.get("paths.validation_output", "") + ) # 执行设置 self.user_dryrun_var.set(self.config.get("execution.dryrun", False)) return @@ -475,32 +517,54 @@ class SettingsTab(ttk.Frame): self.verbose_var.set(self.config.get("extraction.verbose", 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.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_use_database_var.set(self.config.get("validation.use_database", True)) - self.validation_output_filename_var.set(self.config.get("paths.validation_output", "物料状态校验结果.xlsx")) - self.validation_batch_size_var.set(self.config.get("validation.batch_size", 2000)) - self.validation_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", "")) + self.validation_data_source_var.set( + self.config.get("validation.data_source", "database_full") + ) + self.validation_use_database_var.set( + self.config.get("validation.use_database", True) + ) + self.validation_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 设置 - 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_input_width_var.set(self.config.get("ui.production_id_input_width", 20)) 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: # User 用户模式 - 只保存路径设置和执行设置 self.config.set("paths.data_dir", self.data_dir_selector.get()) self.config.set("paths.default_output", self.default_output_var.get()) - self.config.set("paths.validation_output", self.validation_output_filename_var.get()) + self.config.set( + "paths.validation_output", self.validation_output_filename_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.auto_convert", self.auto_convert_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.use_database", self.validation_use_database_var.get()) - self.config.set("paths.validation_output", self.validation_output_filename_var.get()) + self.config.set( + "validation.use_database", self.validation_use_database_var.get() + ) + self.config.set( + "paths.validation_output", self.validation_output_filename_var.get() + ) self.config.set("validation.batch_size", self.validation_batch_size_var.get()) self.config.set("validation.match_mode", self.validation_match_mode_var.get()) - self.config.set("validation.enable_crud_operations", self.validation_enable_crud_var.get()) - self.config.set("validation.default_manager", self.validation_default_manager_var.get()) + self.config.set( + "validation.enable_crud_operations", self.validation_enable_crud_var.get() + ) + self.config.set( + "validation.default_manager", self.validation_default_manager_var.get() + ) # UI 设置 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() # 调用主窗口的 reload_config 方法(如果存在) - if hasattr(main_window, 'reload_config'): + if hasattr(main_window, "reload_config"): main_window.reload_config() except Exception: pass @@ -602,7 +676,7 @@ class SettingsTab(ttk.Frame): database=self.config.get("database.database", ""), user=self.config.get("database.username", ""), password=self.config.get("database.password", ""), - connection_timeout=5 + connection_timeout=5, ) conn.close() messagebox.showinfo("成功", "MySQL 数据库连接测试成功!") @@ -621,9 +695,14 @@ class SettingsTab(ttk.Frame): except ImportError: 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: - messagebox.showerror("错误", "未安装 pyodbc,请运行:\npip install pyodbc") + messagebox.showerror( + "错误", "未安装 pyodbc,请运行:\npip install pyodbc" + ) except Exception as e: messagebox.showerror("错误", f"数据库连接失败:\n{str(e)}") @@ -634,8 +713,11 @@ class SettingsTab(ttk.Frame): def reset_defaults(self): """恢复默认设置""" - if messagebox.askyesno("确认", "确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。"): + if messagebox.askyesno( + "确认", "确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。" + ): from config.schema import AppConfig + self.config.config = AppConfig.from_env() # 重新加载默认配置 self.config.save() self.load_settings() diff --git a/gui/user_selection_dialog.py b/gui/user_selection_dialog.py index 65eb608..ab978b4 100644 --- a/gui/user_selection_dialog.py +++ b/gui/user_selection_dialog.py @@ -1,6 +1,7 @@ """ User Selection Dialog - Allows Admin to choose which user identity to use """ + import tkinter as tk from tkinter import ttk, messagebox from typing import Optional, Dict, Any, List @@ -64,9 +65,7 @@ class UserSelectionDialog: # Title title_label = ttk.Label( - main_frame, - text="请选择要使用的用户身份", - font=('', 14, 'bold') + main_frame, text="请选择要使用的用户身份", font=("", 14, "bold") ) title_label.pack(pady=(0, 20)) @@ -82,12 +81,15 @@ class UserSelectionDialog: # Sort users: current user first, then by username sorted_users = sorted( 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: - username = user['username'] - user_type = user['user_type'] + username = user["username"] + user_type = user["user_type"] is_current = username == self.current_username # Mark current user @@ -99,7 +101,7 @@ class UserSelectionDialog: list_frame, text=display_text, variable=self.selected_var, - value=username + value=username, ) rb.pack(anchor=tk.W, pady=3, padx=5) @@ -111,18 +113,12 @@ class UserSelectionDialog: button_frame.pack(pady=(20, 0)) confirm_btn = ttk.Button( - button_frame, - text="确认", - command=self._on_confirm, - width=10 + button_frame, text="确认", command=self._on_confirm, width=10 ) confirm_btn.pack(side=tk.LEFT, padx=5) cancel_btn = ttk.Button( - button_frame, - text="取消", - command=self._on_cancel, - width=10 + button_frame, text="取消", command=self._on_cancel, width=10 ) cancel_btn.pack(side=tk.LEFT, padx=5) @@ -136,7 +132,7 @@ class UserSelectionDialog: # Find the selected user for user in self.users: - if user['username'] == selected_username: + if user["username"] == selected_username: self.selected_user = user break diff --git a/gui/utils.py b/gui/utils.py index 50169ed..2e97086 100644 --- a/gui/utils.py +++ b/gui/utils.py @@ -61,24 +61,25 @@ def admin_only(func): Returns: 如果是管理员,返回原函数的结果;否则返回 None """ + @functools.wraps(func) def wrapper(self, *args, **kwargs): # 尝试从 self 获取 session_manager - session_manager = getattr(self, 'session_manager', None) + session_manager = getattr(self, "session_manager", None) # 如果没有 session_manager,尝试从 main_window 获取 if session_manager is None: - main_window = getattr(self, 'main_window', None) + main_window = getattr(self, "main_window", None) 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(): return func(self, *args, **kwargs) # 非管理员,记录日志并返回 None - logger = getattr(self, 'logger', None) + logger = getattr(self, "logger", None) if logger: logger.debug(f"权限拒绝: {func.__name__} 需要管理员权限") @@ -103,24 +104,25 @@ def require_session(func): Returns: 如果有有效会话,返回原函数的结果;否则返回 None """ + @functools.wraps(func) def wrapper(self, *args, **kwargs): # 尝试从 self 获取 session_manager - session_manager = getattr(self, 'session_manager', None) + session_manager = getattr(self, "session_manager", None) # 如果没有 session_manager,尝试从 main_window 获取 if session_manager is None: - main_window = getattr(self, 'main_window', None) + main_window = getattr(self, "main_window", None) 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(): return func(self, *args, **kwargs) # 会话无效,记录日志并返回 None - logger = getattr(self, 'logger', None) + logger = getattr(self, "logger", None) if logger: logger.warning(f"会话无效: {func.__name__} 需要有效会话") diff --git a/gui/widgets/__init__.py b/gui/widgets/__init__.py index 5eb79f6..f1b5f25 100644 --- a/gui/widgets/__init__.py +++ b/gui/widgets/__init__.py @@ -12,10 +12,10 @@ from .delete_progress_window import DeleteProgressWindow from .checkbox_treeview import CheckboxTreeview __all__ = [ - 'FileSelector', - 'LogText', - 'ProductionIdInput', - 'GuiTextHandler', - 'DeleteProgressWindow', - 'CheckboxTreeview' + "FileSelector", + "LogText", + "ProductionIdInput", + "GuiTextHandler", + "DeleteProgressWindow", + "CheckboxTreeview", ] diff --git a/gui/widgets/checkbox_treeview.py b/gui/widgets/checkbox_treeview.py index b028f79..9aa6432 100644 --- a/gui/widgets/checkbox_treeview.py +++ b/gui/widgets/checkbox_treeview.py @@ -39,10 +39,10 @@ class CheckboxTreeview(ttk.Treeview): self.on_checkbox_change = on_checkbox_change # checkbox 状态改变回调 # 排序状态 - self.sort_column = None # 当前排序列的列标识符 - self.sort_direction = None # 'asc', 'desc', 或 None + self.sort_column = None # 当前排序列的列标识符 + self.sort_direction = None # 'asc', 'desc', 或 None self.sortable_columns = ["选择", "材料名称"] # 可排序的列白名单 - self.original_headings = {} # 存储原始列标题文本(不含箭头) + self.original_headings = {} # 存储原始列标题文本(不含箭头) # 存储原始列标题(延迟执行以确保标题已设置) self.after(100, self._store_original_headings) @@ -121,7 +121,7 @@ class CheckboxTreeview(ttk.Treeview): # 初始化 checkbox 状态为未选中 checkbox_char = values[0] if values else "☐" - self.checkboxes[item] = (checkbox_char == "☑") + self.checkboxes[item] = checkbox_char == "☑" return item @@ -134,8 +134,8 @@ class CheckboxTreeview(ttk.Treeview): def _store_original_headings(self): """存储原始列标题文本(不含箭头)""" - for col in self['columns']: - self.original_headings[col] = self.heading(col, 'text') + for col in self["columns"]: + self.original_headings[col] = self.heading(col, "text") def _get_column_id_from_column_index(self, column_index): """将列索引 ('#1', '#2') 转换为列标识符 @@ -147,7 +147,7 @@ class CheckboxTreeview(ttk.Treeview): 列标识符,如 '选择', '材料名称' """ index = int(column_index[1:]) - 1 - columns = self['columns'] + columns = self["columns"] if 0 <= index < len(columns): return columns[index] return None @@ -173,15 +173,15 @@ class CheckboxTreeview(ttk.Treeview): # 确定新的排序方向 if self.sort_column == column_id: # 同一列:asc -> desc -> None - if self.sort_direction == 'asc': - new_direction = 'desc' - elif self.sort_direction == 'desc': + if self.sort_direction == "asc": + new_direction = "desc" + elif self.sort_direction == "desc": new_direction = None else: - new_direction = 'asc' + new_direction = "asc" else: # 不同列:从升序开始 - new_direction = 'asc' + new_direction = "asc" # 应用排序 if new_direction: @@ -208,35 +208,33 @@ class CheckboxTreeview(ttk.Treeview): for item in self.get_children(): values = self.item(item, "values") checkbox_state = self.checkboxes.get(item, False) - items_data.append({ - 'item_id': item, - 'values': values, - 'checked': checkbox_state - }) + items_data.append( + {"item_id": item, "values": values, "checked": checkbox_state} + ) # 根据列和方向排序 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 == "材料名称": # 按材料名称排序 items_data.sort( - key=lambda x: str(x['values'][1]) if len(x['values']) > 1 else "", - reverse=(direction == 'desc') + key=lambda x: str(x["values"][1]) if len(x["values"]) > 1 else "", + reverse=(direction == "desc"), ) # 重新排列项目顺序(使用 detach 和 move 保留项目ID和状态) 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): """更新列标题显示(添加/移除排序箭头)""" - for col in self['columns']: + for col in self["columns"]: original = self.original_headings.get(col, col) 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) else: # 移除箭头,显示原始标题 - self.heading(col, text=original) \ No newline at end of file + self.heading(col, text=original) diff --git a/gui/widgets/delete_progress_window.py b/gui/widgets/delete_progress_window.py index 7f0b1be..dd5d909 100644 --- a/gui/widgets/delete_progress_window.py +++ b/gui/widgets/delete_progress_window.py @@ -14,12 +14,14 @@ from datetime import datetime # 尝试导入 tkinterweb 和 markdown2 try: from tkinterweb import HtmlFrame + HAS_TKINTERWEB = True except ImportError: HAS_TKINTERWEB = False try: import markdown2 + HAS_MARKDOWN2 = True except ImportError: HAS_MARKDOWN2 = False @@ -34,7 +36,7 @@ class DeleteProgressWindow: title: str = "执行删除", managers: str = "", 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_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_bar = ttk.Progressbar( - self.progress_frame, - mode='determinate', - length=660, - maximum=100 + self.progress_frame, mode="determinate", length=660, maximum=100 ) self.progress_bar.pack(fill=tk.X, pady=5) @@ -120,15 +121,15 @@ class DeleteProgressWindow: height=10, wrap=tk.WORD, state=tk.DISABLED, - font=('Consolas', 9) + font=("Consolas", 9), ) self.log_text.pack(fill=tk.BOTH, expand=True) # 配置日志标签颜色 - self.log_text.tag_configure('info', foreground='black') - self.log_text.tag_configure('success', foreground='green') - self.log_text.tag_configure('warning', foreground='orange') - self.log_text.tag_configure('error', foreground='red') + self.log_text.tag_configure("info", foreground="black") + self.log_text.tag_configure("success", foreground="green") + self.log_text.tag_configure("warning", foreground="orange") + self.log_text.tag_configure("error", foreground="red") # 报告区域(完成后显示)- 初始隐藏 self.report_frame = ttk.LabelFrame(self.main_frame, text="执行报告", padding=5) @@ -145,7 +146,7 @@ class DeleteProgressWindow: height=20, wrap=tk.WORD, state=tk.DISABLED, - font=('Consolas', 9) + font=("Consolas", 9), ) self.report_text.pack(fill=tk.BOTH, expand=True) @@ -154,18 +155,12 @@ class DeleteProgressWindow: button_frame.pack(fill=tk.X) self.cancel_button = ttk.Button( - button_frame, - text="取消执行", - command=self._on_cancel + button_frame, text="取消执行", command=self._on_cancel ) self.cancel_button.pack(side=tk.RIGHT) # 关闭按钮(初始隐藏) - self.close_button = ttk.Button( - button_frame, - text="关闭", - command=self.close - ) + self.close_button = ttk.Button(button_frame, text="关闭", command=self.close) def _on_cancel(self): """处理取消操作""" @@ -187,7 +182,7 @@ class DeleteProgressWindow: """ if total > 0: percentage = int((current / total) * 100) - self.progress_bar['value'] = percentage + self.progress_bar["value"] = percentage self.progress_var.set(message) else: self.progress_var.set(message) @@ -263,8 +258,7 @@ class DeleteProgressWindow: """ # 使用 markdown2 转换 html_body = markdown2.markdown( - markdown_content, - extras=['tables', 'fenced-code-blocks'] + markdown_content, extras=["tables", "fenced-code-blocks"] ) # 添加样式 @@ -340,59 +334,65 @@ class DeleteProgressWindow: Returns: HTML 内容 """ - lines = markdown_content.split('\n') - html_parts = ['', - ''] + lines = markdown_content.split("\n") + html_parts = [ + '', + "", + ] in_table = False for line in lines: - if line.startswith('# '): - html_parts.append(f'

{line[2:]}

') - elif line.startswith('## '): - html_parts.append(f'

{line[3:]}

') - elif line.startswith('| '): + if line.startswith("# "): + html_parts.append(f"

{line[2:]}

") + elif line.startswith("## "): + html_parts.append(f"

{line[3:]}

") + elif line.startswith("| "): if not in_table: - html_parts.append('') + html_parts.append("
") in_table = True # 检查是否是表头分隔行 - if '|--' in line or '|-' in line: + if "|--" in line or "|-" in line: continue - cells = [cell.strip() for cell in line.split('|')[1:-1]] + cells = [cell.strip() for cell in line.split("|")[1:-1]] if cells: # 第一行作为表头 - if html_parts[-1] == '
': - html_parts.append('' + ''.join(f'' for c in cells) + '') + if html_parts[-1] == "
{c}
": + html_parts.append( + "" + "".join(f"" for c in cells) + "" + ) else: - html_parts.append('' + ''.join(f'' for c in cells) + '') - elif line.startswith('- '): + html_parts.append( + "" + "".join(f"" for c in cells) + "" + ) + elif line.startswith("- "): if in_table: - html_parts.append('
{c}
{c}
{c}
') + html_parts.append("") in_table = False - html_parts.append(f'
  • {line[2:]}
  • ') - elif line.strip() == '': + html_parts.append(f"
  • {line[2:]}
  • ") + elif line.strip() == "": if in_table: - html_parts.append('') + html_parts.append("") in_table = False - html_parts.append('
    ') + html_parts.append("
    ") else: if in_table: - html_parts.append('') + html_parts.append("") in_table = False if line.strip(): - html_parts.append(f'

    {line}

    ') + html_parts.append(f"

    {line}

    ") if in_table: - html_parts.append('') + html_parts.append("") - html_parts.append('') - return '\n'.join(html_parts) + html_parts.append("") + return "\n".join(html_parts) def _markdown_to_text(self, markdown_content: str) -> str: """ @@ -404,34 +404,34 @@ class DeleteProgressWindow: Returns: 格式化后的文本 """ - lines = markdown_content.split('\n') + lines = markdown_content.split("\n") result = [] for line in lines: # 标题 - if line.startswith('# '): - result.append('=' * 60) + if line.startswith("# "): + result.append("=" * 60) result.append(line[2:]) - result.append('=' * 60) - elif line.startswith('## '): - result.append('') + result.append("=" * 60) + elif line.startswith("## "): + result.append("") result.append(line[3:]) - result.append('-' * 40) - elif line.startswith('| '): + result.append("-" * 40) + elif line.startswith("| "): # 表格行 - 保持原样 result.append(line) - elif line.startswith('|--') or line.startswith('|-'): + elif line.startswith("|--") or line.startswith("|-"): # 表格分隔线 - 跳过 continue - elif line.startswith('- '): + elif line.startswith("- "): # 列表项 - result.append(' ' + line) - elif line.strip() == '': - result.append('') + result.append(" " + line) + elif line.strip() == "": + result.append("") else: result.append(line) - return '\n'.join(result) + return "\n".join(result) def close(self): """关闭窗口""" @@ -444,4 +444,4 @@ class DeleteProgressWindow: def set_completed(self): """设置为完成状态""" self.cancel_button.pack_forget() - self.close_button.pack(side=tk.RIGHT) \ No newline at end of file + self.close_button.pack(side=tk.RIGHT) diff --git a/gui/widgets/file_selector.py b/gui/widgets/file_selector.py index c9b87fe..2ccc86a 100644 --- a/gui/widgets/file_selector.py +++ b/gui/widgets/file_selector.py @@ -21,7 +21,7 @@ class FileSelector(ttk.Frame): file_type: str = "file", file_types: list = None, 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": path = filedialog.askopenfilename( - title="选择文件", - initialdir=current_path, - filetypes=self.file_types + title="选择文件", initialdir=current_path, filetypes=self.file_types ) else: # directory - path = filedialog.askdirectory( - title="选择目录", - initialdir=current_path - ) + path = filedialog.askdirectory(title="选择目录", initialdir=current_path) if path: self.entry_var.set(path) diff --git a/gui/widgets/log_handler.py b/gui/widgets/log_handler.py index bd0b386..c3c02da 100644 --- a/gui/widgets/log_handler.py +++ b/gui/widgets/log_handler.py @@ -3,6 +3,7 @@ """ 自定义 logging Handler,将日志输出到 LogText 组件 """ + import logging import re from typing import Optional @@ -29,11 +30,11 @@ class GuiTextHandler(logging.Handler): # 映射 logging 级别到 LogText 级别 self.level_map = { - logging.INFO: 'INFO', - logging.WARNING: 'WARNING', - logging.ERROR: 'ERROR', - logging.DEBUG: 'DEBUG', - logging.CRITICAL: 'ERROR' + logging.INFO: "INFO", + logging.WARNING: "WARNING", + logging.ERROR: "ERROR", + logging.DEBUG: "DEBUG", + logging.CRITICAL: "ERROR", } def set_log_text(self, log_text: LogText): @@ -57,7 +58,7 @@ class GuiTextHandler(logging.Handler): try: # 获取日志级别 - level = self.level_map.get(record.levelno, 'INFO') + level = self.level_map.get(record.levelno, "INFO") # 只获取消息内容,不包含时间戳和级别(LogText.log() 会添加) message = record.getMessage() @@ -78,6 +79,7 @@ class GuiTextHandler(logging.Handler): # 尝试使用 after 确保在主线程更新 import tkinter as tk + try: # 尝试获取主窗口 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) if match: # 移除匹配到的级别前缀 - return message[match.end():] + return message[match.end() :] return message diff --git a/gui/widgets/log_text.py b/gui/widgets/log_text.py index f9c3d28..60cdf3b 100644 --- a/gui/widgets/log_text.py +++ b/gui/widgets/log_text.py @@ -15,11 +15,11 @@ class LogText(tk.Frame): # 日志级别颜色配置 LOG_COLORS = { - 'INFO': '#000000', # 黑色 - 'SUCCESS': '#008000', # 绿色 - 'WARNING': '#FF8C00', # 深橙色 - 'ERROR': '#FF0000', # 红色 - 'DEBUG': '#808080', # 灰色 + "INFO": "#000000", # 黑色 + "SUCCESS": "#008000", # 绿色 + "WARNING": "#FF8C00", # 深橙色 + "ERROR": "#FF0000", # 红色 + "DEBUG": "#808080", # 灰色 } def __init__(self, parent, readonly=True, **kwargs): @@ -67,19 +67,19 @@ class LogText(tk.Frame): def _make_readonly(self): """通过绑定事件使文本框只读""" # 允许复制、全选等常用操作,阻止其他编辑操作 - self.text.bind('', self._handle_key) - self.text.bind('', self._allow_click) # 允许左键点击选择 + self.text.bind("", self._handle_key) + self.text.bind("", self._allow_click) # 允许左键点击选择 def _handle_key(self, event): """处理按键事件,允许复制操作,阻止编辑""" # 允许的快捷键 allowed_keys = [ - 'Control-c', # 复制 - 'Control-C', # 复制(大写) - 'Control-a', # 全选 - 'Control-A', # 全选(大写) - 'Control-x', # 剪切(虽然剪不了,但不报错) - 'Control-X', + "Control-c", # 复制 + "Control-C", # 复制(大写) + "Control-a", # 全选 + "Control-A", # 全选(大写) + "Control-x", # 剪切(虽然剪不了,但不报错) + "Control-X", ] # 检查是否是允许的快捷键 @@ -93,14 +93,14 @@ class LogText(tk.Frame): return # 允许执行 # 其他所有按键都阻止 - return 'break' + return "break" def _allow_click(self, event): """允许点击和选择文本""" # 不打断事件,允许正常的选择操作 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 已配置 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" # 插入文本 tag = level.lower() if self._tags_configured else None if tag: try: - self.text.insert('end', log_message, (tag,)) + self.text.insert("end", log_message, (tag,)) except Exception: # 如果带标签插入失败,尝试不带标签 - self.text.insert('end', log_message) + self.text.insert("end", log_message) 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: """添加 INFO 级别日志""" - self.log(message, 'INFO') + self.log(message, "INFO") def success(self, message: str) -> None: """添加 SUCCESS 级别日志""" - self.log(message, 'SUCCESS') + self.log(message, "SUCCESS") def warning(self, message: str) -> None: """添加 WARNING 级别日志""" - self.log(message, 'WARNING') + self.log(message, "WARNING") def error(self, message: str) -> None: """添加 ERROR 级别日志""" - self.log(message, 'ERROR') + self.log(message, "ERROR") def debug(self, message: str) -> None: """添加 DEBUG 级别日志""" - self.log(message, 'DEBUG') + self.log(message, "DEBUG") 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: """ @@ -163,8 +163,8 @@ class LogText(tk.Frame): 是否成功 """ try: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(self.text.get('1.0', 'end-1c')) + with open(file_path, "w", encoding="utf-8") as f: + f.write(self.text.get("1.0", "end-1c")) return True except Exception as e: self.error(f"保存日志失败: {e}") @@ -182,5 +182,6 @@ class LogText(tk.Frame): def apply_font(self, font_family: str, font_size: int): """应用字体设置""" from tkinter import font as tk_font + font_spec = tk_font.Font(family=font_family, size=font_size) self.text.configure(font=font_spec) diff --git a/gui/widgets/production_id_input.py b/gui/widgets/production_id_input.py index d7f2beb..67f659f 100644 --- a/gui/widgets/production_id_input.py +++ b/gui/widgets/production_id_input.py @@ -35,12 +35,14 @@ class ProductionIdInput(ttk.Frame): justify="center", colors=("black", "#f0f0f0"), bg="#f0f0f0", - width=3 + width=3, ) 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) # 布局:行号 | 文本框 | 滚动条 @@ -74,7 +76,10 @@ class ProductionIdInput(ttk.Frame): 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.configure(foreground="black") diff --git a/gui/widgets/progress_dialog.py b/gui/widgets/progress_dialog.py index 9a3ac41..f9d1273 100644 --- a/gui/widgets/progress_dialog.py +++ b/gui/widgets/progress_dialog.py @@ -20,7 +20,7 @@ class ProgressDialog: title: str = "处理中...", message: str = "请稍候", 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.progress = ttk.Progressbar( - self.dialog, - mode='indeterminate', - length=360 - ) + self.progress = ttk.Progressbar(self.dialog, mode="indeterminate", length=360) self.progress.pack(pady=10, padx=20) self.progress.start(10) @@ -80,9 +76,7 @@ class ProgressDialog: button_frame.pack(pady=10) self.cancel_button = ttk.Button( - button_frame, - text="取消", - command=self._on_cancel + button_frame, text="取消", command=self._on_cancel ) self.cancel_button.pack() @@ -106,8 +100,8 @@ class ProgressDialog: value: 当前进度值 maximum: 最大值 """ - self.progress.config(mode='determinate', maximum=maximum) - self.progress['value'] = value + self.progress.config(mode="determinate", maximum=maximum) + self.progress["value"] = value self.dialog.update_idletasks() def close(self): diff --git a/scripts/migrate_to_env.py b/scripts/migrate_to_env.py index 02ba402..b26ef2b 100644 --- a/scripts/migrate_to_env.py +++ b/scripts/migrate_to_env.py @@ -5,6 +5,7 @@ 将现有的 JSON 配置文件迁移到 .env 环境变量文件 """ + import os import sys import json @@ -21,7 +22,7 @@ from config.schema import AppConfig def migrate_json_to_env( json_file: str = "config/user_settings.json", env_file: str = ".env", - backup: bool = True + backup: bool = True, ) -> bool: """ 迁移 JSON 配置到 .env 文件 @@ -46,7 +47,7 @@ def migrate_json_to_env( # 检查 .env 文件是否已存在 if env_path.exists(): response = input(f"⚠️ .env 文件已存在: {env_path}\n是否覆盖? (y/N): ") - if response.lower() != 'y': + if response.lower() != "y": print("❌ 迁移已取消") return False @@ -63,6 +64,7 @@ def migrate_json_to_env( # 使用 ConfigLoader 将字典转换为配置对象 from config.loader import ConfigLoader + config = ConfigLoader._dict_to_config(json_data) # 保存到 .env 文件 @@ -95,13 +97,13 @@ def migrate_json_to_env( except Exception as e: print(f"❌ 迁移失败: {e}") import traceback + traceback.print_exc() return False def create_env_from_example( - example_file: str = ".env.example", - env_file: str = ".env" + example_file: str = ".env.example", env_file: str = ".env" ) -> bool: """ 从 .env.example 创建 .env 文件 @@ -122,7 +124,7 @@ def create_env_from_example( if env_path.exists(): response = input(f"⚠️ .env 文件已存在: {env_path}\n是否覆盖? (y/N): ") - if response.lower() != 'y': + if response.lower() != "y": print("❌ 操作已取消") return False @@ -160,7 +162,9 @@ def main(): elif command == "migrate": # 从 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" migrate_json_to_env(json_file, env_file) return @@ -192,7 +196,9 @@ def main(): choice = input("\n请输入选项 (1-3): ").strip() 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: json_file = "config/user_settings.json" @@ -201,7 +207,7 @@ def main(): env_file = ".env" 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) diff --git a/utils/discrete_material_plan_cleaner.py b/utils/discrete_material_plan_cleaner.py index 0ccc016..04f562f 100644 --- a/utils/discrete_material_plan_cleaner.py +++ b/utils/discrete_material_plan_cleaner.py @@ -65,16 +65,16 @@ class DiscreteMaterialPlanCleaner: # 统计信息 self.stats = { - 'total_orders': 0, - 'processed_orders': 0, - 'total_materials': 0, # 总物料数 - 'processed_materials': 0, # 已处理物料数 - 'deleted_materials': [], # [{order_id, material_code, material_name}] - 'skipped_materials': [], # [{order_id, material_code, material_name, reason}] - 'unmatched_materials': [], # [{order_id, material_code, material_name}] 不在删除列表的物料 - 'errors': [], # [{order_id, error_message}] - 'start_time': None, - 'end_time': None, + "total_orders": 0, + "processed_orders": 0, + "total_materials": 0, # 总物料数 + "processed_materials": 0, # 已处理物料数 + "deleted_materials": [], # [{order_id, material_code, material_name}] + "skipped_materials": [], # [{order_id, material_code, material_name, reason}] + "unmatched_materials": [], # [{order_id, material_code, material_name}] 不在删除列表的物料 + "errors": [], # [{order_id, error_message}] + "start_time": None, + "end_time": None, } def _log(self, message, level="info"): @@ -100,7 +100,7 @@ class DiscreteMaterialPlanCleaner: total_materials: int, order_id: str, material_name: str, - action: str + action: str, ): """报告物料处理进度 @@ -166,7 +166,9 @@ class DiscreteMaterialPlanCleaner: ) 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: @@ -225,7 +227,7 @@ class DiscreteMaterialPlanCleaner: if detail_status == "审批通过": 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() @@ -252,7 +254,7 @@ class DiscreteMaterialPlanCleaner: # page2.pause() # 调试用,正式运行时可删除 while True: material_idx += 1 - self.stats['processed_materials'] += 1 + self.stats["processed_materials"] += 1 # 稳定性检查:等待行号更新 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_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( - order_index, total_orders, - material_idx, detail_count, - order_id, material_name, "检查" + order_index, + total_orders, + material_idx, + detail_count, + order_id, + material_name, + "检查", ) if material_code in self.to_delete_set: - self._log(f"发现匹配物料: {material_name} ({material_code})") + self._log( + f"发现匹配物料: {material_name} ({material_code})" + ) if (not pending_qty) and not ( row_num_int >= 7000 and row_num_int < 8000 ): # 报告删除进度 self._report_material_progress( - order_index, total_orders, - material_idx, detail_count, - order_id, material_name, "删除" + order_index, + total_orders, + material_idx, + detail_count, + order_id, + material_name, + "删除", ) # 记录删除前的行号 old_row_number = current_row @@ -308,75 +322,98 @@ class DiscreteMaterialPlanCleaner: time.sleep(0.2) else: self._log( - f"⚠️ 等待删除完成超时({max_wait_time}秒)", "warn" + f"⚠️ 等待删除完成超时({max_wait_time}秒)", + "warn", ) # 记录删除统计 if delete_success: - self.stats['deleted_materials'].append({ - 'order_id': order_id, - 'material_code': material_code, - 'material_name': material_name - }) + self.stats["deleted_materials"].append( + { + "order_id": order_id, + "material_code": material_code, + "material_name": material_name, + } + ) continue elif row_num_int >= 7000 and row_num_int < 8000: reason = f"行号 {row_num_int} 在 7000-8000 范围内" self._report_material_progress( - order_index, total_orders, - material_idx, detail_count, - order_id, material_name, "跳过" + order_index, + total_orders, + material_idx, + detail_count, + order_id, + material_name, + "跳过", ) self._log(f"⚠️ {reason},跳过删除", "warn") - self.stats['skipped_materials'].append({ - 'order_id': order_id, - 'material_code': material_code, - 'material_name': material_name, - 'reason': reason - }) + self.stats["skipped_materials"].append( + { + "order_id": order_id, + "material_code": material_code, + "material_name": material_name, + "reason": reason, + } + ) elif pending_qty: reason = f"待发数量为 {pending_qty}" self._report_material_progress( - order_index, total_orders, - material_idx, detail_count, - order_id, material_name, "跳过" + order_index, + total_orders, + material_idx, + detail_count, + order_id, + material_name, + "跳过", ) self._log(f"⚠️ {reason},跳过删除", "warn") - self.stats['skipped_materials'].append({ - 'order_id': order_id, - 'material_code': material_code, - 'material_name': material_name, - 'reason': reason - }) + self.stats["skipped_materials"].append( + { + "order_id": order_id, + "material_code": material_code, + "material_name": material_name, + "reason": reason, + } + ) else: reason = "不满足删除条件" self._report_material_progress( - order_index, total_orders, - material_idx, detail_count, - order_id, material_name, "跳过" + order_index, + total_orders, + material_idx, + detail_count, + order_id, + material_name, + "跳过", ) self._log( f"⚠️ {reason},跳过物料 {material_name} ({material_code})", "warn", ) - self.stats['skipped_materials'].append({ - 'order_id': order_id, - 'material_code': material_code, - 'material_name': material_name, - 'reason': reason - }) + self.stats["skipped_materials"].append( + { + "order_id": order_id, + "material_code": material_code, + "material_name": material_name, + "reason": reason, + } + ) else: self._log( f"ℹ️ 物料 {material_name} ({material_code}) 不在删除列表中,不做处理" ) # 记录到 unmatched_materials - self.stats['unmatched_materials'].append({ - 'order_id': order_id, - 'material_code': material_code, - 'material_name': material_name - }) + self.stats["unmatched_materials"].append( + { + "order_id": order_id, + "material_code": material_code, + "material_name": material_name, + } + ) if self._is_button_enabled(next_btn): last_row_number = current_row next_btn.click() @@ -417,7 +454,7 @@ class DiscreteMaterialPlanCleaner: def clean(self, production_id_file): """执行完整清理流程""" # 初始化统计 - self.stats['start_time'] = datetime.now() + self.stats["start_time"] = datetime.now() # 0. 预加载数据库数据 self.preload_data() @@ -450,20 +487,21 @@ class DiscreteMaterialPlanCleaner: 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): self._log(f"进度: [{index+1}/{len(order_ids)}] 处理单号: {order_id}") try: - self.process_order(inner_frame, order_id, index, page1, len(order_ids)) - self.stats['processed_orders'] += 1 + self.process_order( + inner_frame, order_id, index, page1, len(order_ids) + ) + self.stats["processed_orders"] += 1 except Exception as e: self._log(f"处理单号 {order_id} 时发生异常: {e}", "error") - self.stats['errors'].append({ - 'order_id': order_id, - 'error_message': str(e) - }) + self.stats["errors"].append( + {"order_id": order_id, "error_message": str(e)} + ) continue # 单个失败不影响整体执行 # 登出清理 @@ -473,7 +511,7 @@ class DiscreteMaterialPlanCleaner: self._log("=" * 30 + " 任务全部完成 " + "=" * 30) # 记录结束时间 - self.stats['end_time'] = datetime.now() + self.stats["end_time"] = datetime.now() def generate_report(self) -> str: """生成 Markdown 格式的执行报告 @@ -490,62 +528,78 @@ class DiscreteMaterialPlanCleaner: # 概述 report_lines.append("## 概述") report_lines.append("") - start_time = self.stats.get('start_time') - end_time = self.stats.get('end_time') + start_time = self.stats.get("start_time") + end_time = self.stats.get("end_time") duration = None if start_time and end_time: duration = end_time - start_time - report_lines.append(f"- 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}") + report_lines.append( + f"- 开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}" + ) report_lines.append(f"- 结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}") report_lines.append(f"- 执行时长: {duration}") - report_lines.append(f"- 处理订单: {self.stats['processed_orders']}/{self.stats['total_orders']} 个") - report_lines.append(f"- 处理物料: {self.stats['processed_materials']}/{self.stats['total_materials']} 条") + report_lines.append( + f"- 处理订单: {self.stats['processed_orders']}/{self.stats['total_orders']} 个" + ) + report_lines.append( + f"- 处理物料: {self.stats['processed_materials']}/{self.stats['total_materials']} 条" + ) report_lines.append(f"- 删除物料: {len(self.stats['deleted_materials'])} 条") report_lines.append(f"- 跳过物料: {len(self.stats['skipped_materials'])} 条") - report_lines.append(f"- 未处理物料: {len(self.stats['unmatched_materials'])} 条") + report_lines.append( + f"- 未处理物料: {len(self.stats['unmatched_materials'])} 条" + ) 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: report_lines.append(f"- 负责人: {', '.join(self.manager_names)}") 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("|--------|----------|----------|") - for item in self.stats['deleted_materials']: - report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |") + for item in self.stats["deleted_materials"]: + report_lines.append( + f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |" + ) report_lines.append("") # 跳过明细 - if self.stats['skipped_materials']: + if self.stats["skipped_materials"]: report_lines.append("## 跳过明细") report_lines.append("") report_lines.append("| 订单号 | 物料编码 | 物料名称 | 跳过原因 |") report_lines.append("|--------|----------|----------|----------|") - for item in self.stats['skipped_materials']: - report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} | {item['reason']} |") + for item in self.stats["skipped_materials"]: + report_lines.append( + f"| {item['order_id']} | {item['material_code']} | {item['material_name']} | {item['reason']} |" + ) report_lines.append("") # 未处理物料明细 - if self.stats['unmatched_materials']: + if self.stats["unmatched_materials"]: report_lines.append("## 未处理物料") report_lines.append("") report_lines.append("| 订单号 | 物料编码 | 物料名称 |") report_lines.append("|--------|----------|----------|") - for item in self.stats['unmatched_materials']: - report_lines.append(f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |") + for item in self.stats["unmatched_materials"]: + report_lines.append( + f"| {item['order_id']} | {item['material_code']} | {item['material_name']} |" + ) report_lines.append("") # 错误明细 - if self.stats['errors']: + if self.stats["errors"]: 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("") diff --git a/utils/discrete_material_plan_extractor.py b/utils/discrete_material_plan_extractor.py index bb3b796..b23bcce 100644 --- a/utils/discrete_material_plan_extractor.py +++ b/utils/discrete_material_plan_extractor.py @@ -27,22 +27,24 @@ except ImportError: # --- 全局日志配置 --- # 调整格式:增加 [] 使其与 UI 控件的默认风格保持一致 -LOG_FORMAT = '[%(asctime)s] [%(levelname)s] %(message)s' -DATE_FORMAT = '%Y-%m-%d %H:%M:%S' +LOG_FORMAT = "[%(asctime)s] [%(levelname)s] %(message)s" +DATE_FORMAT = "%Y-%m-%d %H:%M:%S" -logging.basicConfig( - level=logging.INFO, - format=LOG_FORMAT, - datefmt=DATE_FORMAT -) +logging.basicConfig(level=logging.INFO, format=LOG_FORMAT, datefmt=DATE_FORMAT) logger = logging.getLogger(__name__) + class DiscreteMaterialPlanExtractor: """离散备料计划维护数据提取器""" def __init__( - self, username, password, headless=False, verbose=True, batch_size=100, - enable_db_persistence=False + self, + username, + password, + headless=False, + verbose=True, + batch_size=100, + enable_db_persistence=False, ): self.username = username self.password = password @@ -53,10 +55,11 @@ class DiscreteMaterialPlanExtractor: self.converter = ExcelConverter(verbose=verbose) self.enable_db_persistence = enable_db_persistence self.dao = None - + if self.enable_db_persistence: try: from db.discrete_material_plan_dao import DiscreteMaterialPlanDAO + self.dao = DiscreteMaterialPlanDAO() except ImportError: self._log("无法加载数据库 DAO 模块,持久化功能将不可用", "error") @@ -67,11 +70,7 @@ class DiscreteMaterialPlanExtractor: """ level = level.lower() # 1. 记录到标准控制台 - log_map = { - "info": logger.info, - "warn": logger.warning, - "error": logger.error - } + log_map = {"info": logger.info, "warn": logger.warning, "error": logger.error} log_func = log_map.get(level, logger.info) log_func(message) @@ -80,7 +79,9 @@ class DiscreteMaterialPlanExtractor: if self.progress_callback: self._report_progress("log", 0, 0, message, log_level=level.upper()) - def _report_progress(self, stage: str, current: int, total: int, message: str, **detail): + def _report_progress( + self, stage: str, current: int, total: int, message: str, **detail + ): """标准化进度汇报""" if self.progress_callback and ProgressInfo: try: @@ -98,20 +99,30 @@ class DiscreteMaterialPlanExtractor: def get_production_order_numbers(self, production_id_file, report_progress=False): """读取总排号并查询数据库获取生产订单号""" 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) self._log(f"文件读取完成: 找到 {len(production_ids)} 个 Production ID") 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) self._log(f"数据库查询完成: 共匹配到 {len(order_ids)} 条生产订单号") if report_progress: - self._report_progress("query", 3, 3, "订单号查询阶段结束", action="query_complete") - + self._report_progress( + "query", 3, 3, "订单号查询阶段结束", action="query_complete" + ) + return order_ids 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): """执行单批次数据的下载流程""" - self._report_progress("download", batch_index * 7 + 1, total_batches * 7, - f"第 {batch_index + 1} 批: 正在填充订单号", action="fill_orders") - + self._report_progress( + "download", + batch_index * 7 + 1, + total_batches * 7, + f"第 {batch_index + 1} 批: 正在填充订单号", + action="fill_orders", + ) + textbox = inner_frame.get_by_role("textbox", name="来源生产订单号") textbox.fill("") 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("button", name="更多").hover() 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") 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 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) all_dfs.append(df) self._log(f"文件 {i} 转换完成: 提取到 {len(df)} 条记录") 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.to_excel(output_path, index=False) - + for p in file_paths: - try: os.remove(p) - except: pass - + try: + os.remove(p) + except: + pass + return output_path, merged_df return None, None def _save_to_database(self, df: pd.DataFrame): """将结果存入数据库并打印详细统计信息""" - if not self.dao: return + if not self.dao: + return try: self._report_progress("database", 1, 3, "正在将数据同步至数据库...") # 使用 with 关键字确保资源安全释放 with self.dao as db: stats = db.save_dataframe_with_replace(df) - + # 保留并输出完整的处理细节:删除条数和新增条数 msg = f"数据库保存完成: 删除 {stats.get('deleted', 0)} 条, 新增 {stats.get('inserted', 0)} 条" self._log(msg, "info") - + except Exception as e: self._log(f"数据库保存失败: {str(e)}", "error") @@ -209,8 +239,10 @@ class DiscreteMaterialPlanExtractor: input_box.press("Enter") def extract( - self, production_id_file, output_file="D:/python/playwrite/data/离散备料计划维护_合并.xlsx", - progress_callback=None + self, + production_id_file, + output_file="D:/python/playwrite/data/离散备料计划维护_合并.xlsx", + progress_callback=None, ): """主入口:执行全流程数据提取任务""" self.progress_callback = progress_callback @@ -220,15 +252,22 @@ class DiscreteMaterialPlanExtractor: with sync_playwright() as playwright: self._report_progress("login", 1, 3, "启动浏览器并尝试登录 ERP...") browser, context, page, main_frame = login( - playwright=playwright, username=self.username, password=self.password, - headless=self.headless, ignore_https_errors=True + playwright=playwright, + username=self.username, + password=self.password, + headless=self.headless, + ignore_https_errors=True, + ) + + self._log( + "======================================== 开始执行数据提取任务 ========================================" ) - self._log("======================================== 开始执行数据提取任务 ========================================") - main_frame.locator("i").first.click() 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 f_frame = page1.locator("#forwardFrame").content_frame @@ -237,16 +276,22 @@ class DiscreteMaterialPlanExtractor: work_frame = inner_frame_locator.content_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)) for i, batch_ids in enumerate(batch_list): self._log(f"正在处理第 {i+1} 批次 (共 {len(batch_list)} 批)") 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) except Exception as e: - self._log(f"批次 {i+1} 处理异常,已跳过。详细错误: {e}", "error") + self._log( + f"批次 {i+1} 处理异常,已跳过。详细错误: {e}", "error" + ) continue self._log("正在注销并关闭浏览器环境...") @@ -255,28 +300,32 @@ class DiscreteMaterialPlanExtractor: browser.close() 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: self._save_to_database(final_df) - + self._log(f"所有流程已顺利结束,结果文件: {final_path}") self._report_progress("complete", 1, 1, "任务完成") return final_path - + self._log("未获得任何有效数据,任务终止", "warn") return None finally: self.progress_callback = None + def main(): extractor = DiscreteMaterialPlanExtractor( username="BLDpengqiangqiang", password="your_password", - enable_db_persistence=True + enable_db_persistence=True, ) id_file = os.path.join(os.path.dirname(__file__), "productionID.txt") extractor.extract(id_file) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/utils/material_status_validator.py b/utils/material_status_validator.py index 43dc165..5566f59 100644 --- a/utils/material_status_validator.py +++ b/utils/material_status_validator.py @@ -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_records_dao import MaterialsToBeDeletedDAO - # ==================== DATA STRUCTURES ==================== + @dataclass class MaterialValidationResult: """Enhanced material validation result with complete record information""" + material_name: str material_code: str specification: Optional[str] = None model: Optional[str] = None manager_name: Optional[str] = None 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: @@ -261,13 +264,11 @@ class MaterialStatusValidator: Returns: 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()] return items - def _get_source_numbers_from_inputs( - self, inputs: List[str] - ) -> List[str]: + def _get_source_numbers_from_inputs(self, inputs: List[str]) -> List[str]: """ 根据输入列表智能获取 SourceNumber(生产订单号)列表 @@ -281,7 +282,7 @@ class MaterialStatusValidator: List[str]: 生产订单号列表 """ production_ids = [] # 需要查询数据库的 - order_numbers = [] # 直接使用的 + order_numbers = [] # 直接使用的 for item in inputs: input_type = self._identify_input_type(item) @@ -298,7 +299,9 @@ class MaterialStatusValidator: # 查询数据库获取总排号对应的生产订单号 if production_ids: - self._print(f"[INFO] 正在查询 {len(production_ids)} 个总排号对应的生产订单号...") + self._print( + f"[INFO] 正在查询 {len(production_ids)} 个总排号对应的生产订单号..." + ) contract_dao = ProductionContractDataDAO() db_order_numbers = contract_dao.get_source_numbers_by_总排号(production_ids) self._print(f"[INFO] 从数据库获取到 {len(db_order_numbers)} 个生产订单号") @@ -307,7 +310,9 @@ class MaterialStatusValidator: # 去重 unique_order_numbers = list(dict.fromkeys(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 @@ -326,7 +331,9 @@ class MaterialStatusValidator: if source_numbers is None or not source_numbers: self._print("[INFO] 查询所有材料的名称...") else: - self._print(f"[INFO] 查询 {len(source_numbers)} 个生产订单对应的材料名称...") + self._print( + f"[INFO] 查询 {len(source_numbers)} 个生产订单对应的材料名称..." + ) dao = DiscreteMaterialPlanDAO() material_names = dao.get_unique_material_names(source_numbers) @@ -338,7 +345,7 @@ class MaterialStatusValidator: self, production_id_file: str = None, full_table: bool = False, - output_file: str = None + output_file: str = None, ) -> str: """ 使用数据库作为数据源执行校验 @@ -392,9 +399,7 @@ class MaterialStatusValidator: # 3. 获取材料名称 material_names = self._get_material_names_from_db(source_numbers) else: - raise ValueError( - "必须指定 full_table=True 或提供 production_id_file 参数" - ) + raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数") # 从数据库获取待删除物料 self._print("\n从数据库获取待删除物料...") @@ -426,7 +431,9 @@ class MaterialStatusValidator: self, material_records: 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]: """ Match materials with detailed information. @@ -442,14 +449,16 @@ class MaterialStatusValidator: results = [] for record in material_records: - material_name = record.get('MaterialName', '') or '' - material_code = record.get('MaterialCode', '') or '' - specification = record.get('Specification', '') or None - model = record.get('Model', '') or None + material_name = record.get("MaterialName", "") or "" + material_code = record.get("MaterialCode", "") or "" + specification = record.get("Specification", "") or None + model = record.get("Model", "") or None # Priority 1: Check MaterialsToBeDeleted (MaterialCode exact match) # 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 matched_keyword = None @@ -457,10 +466,10 @@ class MaterialStatusValidator: # (MaterialName contains match) if not manager_name: 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: matched_keyword = type_material_name - manager_name = type_record.get('ManagerName') + manager_name = type_record.get("ManagerName") break result = MaterialValidationResult( @@ -470,7 +479,7 @@ class MaterialStatusValidator: model=model, manager_name=manager_name, is_marked_for_deletion=is_marked, - matched_type_keyword=matched_keyword + matched_type_keyword=matched_keyword, ) results.append(result) @@ -480,7 +489,7 @@ class MaterialStatusValidator: self, production_id_file: str = None, full_table: bool = False, - output_file: str = None + output_file: str = None, ) -> tuple: """ Enhanced database validation with complete record information. @@ -510,7 +519,9 @@ class MaterialStatusValidator: # Get material records (complete records, not just MaterialName) if full_table: self._print("\n模式: 全表校验") - self._print("[INFO] 查询 DiscreteMaterialPlanData 表中的所有完整记录(启用 MaterialCode 去重)...") + self._print( + "[INFO] 查询 DiscreteMaterialPlanData 表中的所有完整记录(启用 MaterialCode 去重)..." + ) dao = DiscreteMaterialPlanDAO() # Get original count for deduplication statistics @@ -521,7 +532,9 @@ class MaterialStatusValidator: self._print(f"[INFO] 获取到 {len(material_records)} 条记录") if dedup_count > 0: - self._print(f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录") + self._print( + f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录" + ) elif production_id_file: self._print("\n模式: 输入过滤校验") self._print(f"[INFO] 读取输入文件: {production_id_file}") @@ -542,7 +555,9 @@ class MaterialStatusValidator: return output_file, [] # 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() # Get original count for deduplication statistics @@ -554,7 +569,9 @@ class MaterialStatusValidator: self._print(f"[INFO] 获取到 {len(material_records)} 条记录") if dedup_count > 0: - self._print(f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录") + self._print( + f"[INFO] 基于 MaterialCode 去重:移除了 {dedup_count} 条重复记录" + ) # 如果没有找到物料记录,给出友好提示 if not material_records: @@ -563,7 +580,9 @@ class MaterialStatusValidator: self._print("[ERROR] 1. 这些生产订单的物料数据还没有提取到数据库") self._print("[ERROR] 2. 请先运行【正式备料计划数据提取】工具") self._print("[ERROR] 3. 提取时勾选【持久化到数据库】选项") - self._print(f"[ERROR] 4. 将这些输入项的物料数据保存到 DiscreteMaterialPlanData 表") + self._print( + f"[ERROR] 4. 将这些输入项的物料数据保存到 DiscreteMaterialPlanData 表" + ) else: raise ValueError("必须指定 full_table=True 或提供 production_id_file 参数") @@ -580,15 +599,17 @@ class MaterialStatusValidator: # Build dictionary: MaterialCode -> ManagerName marked_codes_dict = { - r['MaterialCode']: r['ManagerName'] + r["MaterialCode"]: r["ManagerName"] 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)} 个已标记的物料代码") # Match materials 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 self._print("\n输出结果...") @@ -596,15 +617,17 @@ class MaterialStatusValidator: # Convert to DataFrame for Excel export df_data = [] for r in results: - df_data.append({ - "材料名称": r.material_name, - "材料代码": r.material_code, - "规格": r.specification or '', - "型号": r.model or '', - "负责人": r.manager_name or '', - "已标记删除": "是" if r.is_marked_for_deletion else "否", - "匹配的关键词": r.matched_type_keyword or '' - }) + df_data.append( + { + "材料名称": r.material_name, + "材料代码": r.material_code, + "规格": r.specification or "", + "型号": r.model or "", + "负责人": r.manager_name or "", + "已标记删除": "是" if r.is_marked_for_deletion else "否", + "匹配的关键词": r.matched_type_keyword or "", + } + ) result_df = pd.DataFrame(df_data) result_df.to_excel(output_file, index=False)