feat: add database-driven material validation with multi-mode support
This commit enhances the material validation functionality to support database-driven workflows alongside the existing Excel-based approach. ## New Features ### DAO Layer - Add ProductionContractDataDAO for querying production contract data - Add MaterialsToBeDeletedDAO with full CRUD operations - Enhance DiscreteMaterialPlanDAO with query_all(), query_by_source_numbers(), and get_unique_material_names() methods ### Validation Modes Support for 4 validation modes in MaterialValidationTab: 1. Database Full - Query all materials from DiscreteMaterialPlanData 2. Database Filtered - Query by ProductionID.txt file 3. Excel Existing - Validate from existing Excel file 4. Excel Full - Complete workflow with ERP extraction ### Configuration - Add ValidationConfig dataclass with data_source, batch_size, match_mode, enable_crud_operations, and default_manager fields - Update ConfigLoader to support validation configuration - Add validation settings section in SettingsTab GUI ### Query Chain Implementation of full query chain: ProductionID.txt (总排号) → productionContractData (生产订单号) → DiscreteMaterialPlanData (SourceNumber) → MaterialName → MaterialsToBeDeleted comparison ## Backward Compatibility All existing Excel-based validation methods remain unchanged, ensuring no breaking changes for existing workflows. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from config.schema import (
|
||||
DatabaseConfig,
|
||||
PathConfig,
|
||||
ExtractionConfig,
|
||||
ValidationConfig,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
@@ -45,6 +46,14 @@ DEFAULT_APP_CONFIG = AppConfig(
|
||||
merge_batches=True,
|
||||
enable_db_persistence=False, # Disabled by default
|
||||
),
|
||||
validation=ValidationConfig(
|
||||
data_source="database_full",
|
||||
use_database=True,
|
||||
batch_size=2000,
|
||||
enable_crud_operations=False,
|
||||
default_manager="",
|
||||
match_mode="substring",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ class ConfigLoader:
|
||||
database_dict = settings.get("database", {})
|
||||
paths_dict = settings.get("paths", {})
|
||||
extraction_dict = settings.get("extraction", {})
|
||||
validation_dict = settings.get("validation", {})
|
||||
|
||||
return AppConfig(
|
||||
erp=ERPConfig(
|
||||
@@ -142,9 +143,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),
|
||||
),
|
||||
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),
|
||||
default_manager=validation_dict.get("default_manager", ""),
|
||||
match_mode=validation_dict.get("match_mode", "substring"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# 为了兼容旧代码,导入必要的类型
|
||||
from config.schema import ERPConfig, DatabaseConfig, PathConfig, ExtractionConfig
|
||||
from config.schema import ERPConfig, DatabaseConfig, PathConfig, ExtractionConfig, ValidationConfig
|
||||
|
||||
@@ -97,6 +97,48 @@ class ExtractionConfig:
|
||||
return errors
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationConfig:
|
||||
"""物料校验配置"""
|
||||
|
||||
data_source: str = "database_full"
|
||||
use_database: bool = True
|
||||
batch_size: int = 2000
|
||||
enable_crud_operations: bool = False
|
||||
default_manager: str = ""
|
||||
match_mode: str = "substring"
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
"""验证配置,返回错误列表"""
|
||||
errors = []
|
||||
|
||||
valid_sources = [
|
||||
"database_full",
|
||||
"database_filtered",
|
||||
"excel_existing",
|
||||
"excel_full"
|
||||
]
|
||||
if self.data_source not in valid_sources:
|
||||
errors.append(
|
||||
f"无效的数据源: {self.data_source}。"
|
||||
f"有效选项: {', '.join(valid_sources)}"
|
||||
)
|
||||
|
||||
if self.batch_size <= 0:
|
||||
errors.append("批次大小必须大于 0")
|
||||
if self.batch_size > 2000:
|
||||
errors.append("批次大小不应超过 2000(SQL Server 参数限制)")
|
||||
|
||||
valid_match_modes = ["substring", "exact"]
|
||||
if self.match_mode not in valid_match_modes:
|
||||
errors.append(
|
||||
f"无效的匹配模式: {self.match_mode}。"
|
||||
f"有效选项: {', '.join(valid_match_modes)}"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""应用总配置"""
|
||||
@@ -105,6 +147,7 @@ class AppConfig:
|
||||
database: DatabaseConfig
|
||||
paths: PathConfig
|
||||
extraction: ExtractionConfig
|
||||
validation: ValidationConfig
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
"""验证所有配置,返回错误列表"""
|
||||
@@ -113,6 +156,7 @@ class AppConfig:
|
||||
errors.extend(self.database.validate())
|
||||
errors.extend(self.paths.validate())
|
||||
errors.extend(self.extraction.validate())
|
||||
errors.extend(self.validation.validate())
|
||||
return errors
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -147,4 +191,12 @@ class AppConfig:
|
||||
"merge_batches": self.extraction.merge_batches,
|
||||
"enable_db_persistence": self.extraction.enable_db_persistence,
|
||||
},
|
||||
"validation": {
|
||||
"data_source": self.validation.data_source,
|
||||
"use_database": self.validation.use_database,
|
||||
"batch_size": self.validation.batch_size,
|
||||
"enable_crud_operations": self.validation.enable_crud_operations,
|
||||
"default_manager": self.validation.default_manager,
|
||||
"match_mode": self.validation.match_mode,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ class DiscreteMaterialPlanDAO:
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exit context manager and close database connection"""
|
||||
if self.db:
|
||||
self.db.close()
|
||||
self.db.disconnect()
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
if self.db:
|
||||
self.db.close()
|
||||
self.db.disconnect()
|
||||
|
||||
def save_dataframe_with_replace(self, df: pd.DataFrame) -> Dict[str, int]:
|
||||
"""
|
||||
@@ -311,3 +311,100 @@ class DiscreteMaterialPlanDAO:
|
||||
"""
|
||||
result = db.execute_query(sql)
|
||||
return result[0] if result else {}
|
||||
|
||||
# ==================== ENHANCED QUERY METHODS ====================
|
||||
|
||||
def query_all(self) -> List[Dict]:
|
||||
"""
|
||||
Query all records from DiscreteMaterialPlanData table.
|
||||
|
||||
Returns:
|
||||
List of dictionaries representing all records
|
||||
"""
|
||||
with get_connection() as db:
|
||||
sql = "SELECT * FROM DiscreteMaterialPlanData"
|
||||
return db.execute_query(sql)
|
||||
|
||||
def query_by_source_numbers(self, source_numbers: List[str]) -> List[Dict]:
|
||||
"""
|
||||
Query records by SourceNumber list (生产订单号).
|
||||
|
||||
Args:
|
||||
source_numbers: List of SourceNumber values to query
|
||||
|
||||
Returns:
|
||||
List of dictionaries representing records
|
||||
"""
|
||||
if not source_numbers:
|
||||
return []
|
||||
|
||||
# SQL Server parameter limit requires batching
|
||||
batch_size = 2000
|
||||
all_results = []
|
||||
|
||||
for i in range(0, len(source_numbers), batch_size):
|
||||
batch = source_numbers[i:i + batch_size]
|
||||
placeholders = ','.join(['?' for _ in batch])
|
||||
sql = f"SELECT * FROM DiscreteMaterialPlanData WHERE SourceNumber IN ({placeholders})"
|
||||
with get_connection() as db:
|
||||
results = db.execute_query(sql, tuple(batch))
|
||||
all_results.extend(results)
|
||||
|
||||
return all_results
|
||||
|
||||
def get_unique_material_names(self, source_numbers: List[str] = None) -> List[str]:
|
||||
"""
|
||||
Get unique material names, optionally filtered by SourceNumber.
|
||||
|
||||
Args:
|
||||
source_numbers: Optional list of SourceNumber values to filter by
|
||||
|
||||
Returns:
|
||||
List of unique material names
|
||||
"""
|
||||
if source_numbers is None or not source_numbers:
|
||||
# No filter - get all unique material names
|
||||
sql = "SELECT DISTINCT MaterialName FROM DiscreteMaterialPlanData 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')]
|
||||
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]
|
||||
placeholders = ','.join(['?' for _ in batch])
|
||||
sql = f"""
|
||||
SELECT DISTINCT MaterialName
|
||||
FROM DiscreteMaterialPlanData
|
||||
WHERE SourceNumber IN ({placeholders})
|
||||
AND MaterialName IS NOT NULL
|
||||
"""
|
||||
with get_connection() as db:
|
||||
results = db.execute_query(sql, tuple(batch))
|
||||
batch_materials = [r['MaterialName'] for r in results if r.get('MaterialName')]
|
||||
all_material_names.update(batch_materials)
|
||||
|
||||
return list(all_material_names)
|
||||
|
||||
def get_material_names_by_总排号(self, 总排号_list: List[str]) -> List[str]:
|
||||
"""
|
||||
Get unique material names by 总排号 list.
|
||||
This method combines query from production contract data and discrete material plan.
|
||||
|
||||
Args:
|
||||
总排号_list: List of 总排号 values
|
||||
|
||||
Returns:
|
||||
List of unique material names
|
||||
"""
|
||||
from db.production_contract_data_dao import ProductionContractDataDAO
|
||||
|
||||
# First get SourceNumbers from production contract data
|
||||
contract_dao = ProductionContractDataDAO()
|
||||
source_numbers = contract_dao.get_source_numbers_by_总排号(总排号_list)
|
||||
|
||||
# Then get material names filtered by these SourceNumbers
|
||||
return self.get_unique_material_names(source_numbers)
|
||||
|
||||
345
db/materials_to_be_deleted_dao.py
Normal file
345
db/materials_to_be_deleted_dao.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
Data Access Object for MaterialsToBeDeleted table.
|
||||
|
||||
This module provides CRUD operations for the MaterialsToBeDeleted table,
|
||||
which tracks materials that need to be deleted by their managers.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
from db.connection import get_connection
|
||||
|
||||
|
||||
class MaterialsToBeDeletedDAO:
|
||||
"""Data Access Object for MaterialsToBeDeleted table CRUD operations"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = None
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter context manager and establish database connection"""
|
||||
self.db = get_connection()
|
||||
self.db.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exit context manager and close database connection"""
|
||||
if self.db:
|
||||
self.db.disconnect()
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
if self.db:
|
||||
self.db.disconnect()
|
||||
|
||||
# ==================== CREATE ====================
|
||||
|
||||
def insert_material(
|
||||
self, material_name: str, manager_name: str
|
||||
) -> bool:
|
||||
"""
|
||||
Insert a single material record.
|
||||
|
||||
Args:
|
||||
material_name: Material name
|
||||
manager_name: Manager name
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
sql = """
|
||||
INSERT INTO [dbo].[MaterialsToBeDeleted] ([MaterialName], [ManagerName])
|
||||
VALUES (?, ?)
|
||||
"""
|
||||
try:
|
||||
with get_connection() as db:
|
||||
db.execute_update(sql, (material_name, manager_name))
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error inserting material: {e}")
|
||||
return False
|
||||
|
||||
def insert_batch(self, materials: List[Tuple[str, str]]) -> int:
|
||||
"""
|
||||
Insert multiple material records in batch.
|
||||
|
||||
Args:
|
||||
materials: List of tuples (material_name, manager_name)
|
||||
|
||||
Returns:
|
||||
Number of records inserted
|
||||
"""
|
||||
if not materials:
|
||||
return 0
|
||||
|
||||
sql = """
|
||||
INSERT INTO [dbo].[MaterialsToBeDeleted] ([MaterialName], [ManagerName])
|
||||
VALUES (?, ?)
|
||||
"""
|
||||
|
||||
inserted_count = 0
|
||||
try:
|
||||
with get_connection() as db:
|
||||
for material_name, manager_name in materials:
|
||||
db.execute_update(sql, (material_name, manager_name))
|
||||
inserted_count += 1
|
||||
return inserted_count
|
||||
except Exception as e:
|
||||
print(f"Error inserting batch materials: {e}")
|
||||
return inserted_count
|
||||
|
||||
# ==================== READ ====================
|
||||
|
||||
def get_all_materials(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all material records.
|
||||
|
||||
Returns:
|
||||
List of all materials with MaterialName and ManagerName
|
||||
"""
|
||||
sql = """
|
||||
SELECT [MaterialName], [ManagerName]
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [MaterialName] IS NOT NULL
|
||||
ORDER BY [ManagerName], [MaterialName]
|
||||
"""
|
||||
with get_connection() as db:
|
||||
return db.execute_query(sql)
|
||||
|
||||
def get_materials_by_manager(self, manager_name: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Get all materials for a specific manager.
|
||||
|
||||
Args:
|
||||
manager_name: Manager name
|
||||
|
||||
Returns:
|
||||
List of materials for the specified manager
|
||||
"""
|
||||
sql = """
|
||||
SELECT [MaterialName], [ManagerName]
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [ManagerName] = ? AND [MaterialName] IS NOT NULL
|
||||
ORDER BY [MaterialName]
|
||||
"""
|
||||
with get_connection() as db:
|
||||
return db.execute_query(sql, (manager_name,))
|
||||
|
||||
def get_managers(self) -> List[str]:
|
||||
"""
|
||||
Get list of unique manager names.
|
||||
|
||||
Returns:
|
||||
List of unique manager names
|
||||
"""
|
||||
sql = """
|
||||
SELECT DISTINCT [ManagerName]
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [ManagerName] IS NOT NULL
|
||||
ORDER BY [ManagerName]
|
||||
"""
|
||||
with get_connection() as db:
|
||||
results = db.execute_query(sql)
|
||||
return [r['ManagerName'] for r in results if r.get('ManagerName')]
|
||||
|
||||
def get_material_names_by_manager(self, manager_name: str) -> List[str]:
|
||||
"""
|
||||
Get material names for a specific manager.
|
||||
|
||||
Args:
|
||||
manager_name: Manager name
|
||||
|
||||
Returns:
|
||||
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')]
|
||||
|
||||
# ==================== UPDATE ====================
|
||||
|
||||
def update_manager(
|
||||
self,
|
||||
material_name: str,
|
||||
old_manager: str,
|
||||
new_manager: str
|
||||
) -> bool:
|
||||
"""
|
||||
Update manager for a specific material.
|
||||
|
||||
Args:
|
||||
material_name: Material name
|
||||
old_manager: Current manager name
|
||||
new_manager: New manager name
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
sql = """
|
||||
UPDATE [dbo].[MaterialsToBeDeleted]
|
||||
SET [ManagerName] = ?
|
||||
WHERE [MaterialName] = ? AND [ManagerName] = ?
|
||||
"""
|
||||
try:
|
||||
with get_connection() as db:
|
||||
affected = db.execute_update(sql, (new_manager, material_name, old_manager))
|
||||
return affected > 0
|
||||
except Exception as e:
|
||||
print(f"Error updating manager: {e}")
|
||||
return False
|
||||
|
||||
# ==================== DELETE ====================
|
||||
|
||||
def delete_material(
|
||||
self,
|
||||
material_name: str,
|
||||
manager_name: str
|
||||
) -> bool:
|
||||
"""
|
||||
Delete a specific material record.
|
||||
|
||||
Args:
|
||||
material_name: Material name
|
||||
manager_name: Manager name
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
sql = """
|
||||
DELETE FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [MaterialName] = ? AND [ManagerName] = ?
|
||||
"""
|
||||
try:
|
||||
with get_connection() as db:
|
||||
affected = db.execute_update(sql, (material_name, manager_name))
|
||||
return affected > 0
|
||||
except Exception as e:
|
||||
print(f"Error deleting material: {e}")
|
||||
return False
|
||||
|
||||
def delete_by_manager(self, manager_name: str) -> int:
|
||||
"""
|
||||
Delete all materials for a specific manager.
|
||||
|
||||
Args:
|
||||
manager_name: Manager name
|
||||
|
||||
Returns:
|
||||
Number of records deleted
|
||||
"""
|
||||
sql = """
|
||||
DELETE FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [ManagerName] = ?
|
||||
"""
|
||||
try:
|
||||
with get_connection() as db:
|
||||
return db.execute_update(sql, (manager_name,))
|
||||
except Exception as e:
|
||||
print(f"Error deleting by manager: {e}")
|
||||
return 0
|
||||
|
||||
def delete_all_materials(self) -> int:
|
||||
"""
|
||||
Delete all material records.
|
||||
|
||||
Returns:
|
||||
Number of records deleted
|
||||
"""
|
||||
sql = "DELETE FROM [dbo].[MaterialsToBeDeleted]"
|
||||
try:
|
||||
with get_connection() as db:
|
||||
return db.execute_update(sql)
|
||||
except Exception as e:
|
||||
print(f"Error deleting all materials: {e}")
|
||||
return 0
|
||||
|
||||
# ==================== UTILITIES ====================
|
||||
|
||||
def material_exists(self, material_name: str) -> bool:
|
||||
"""
|
||||
Check if a material exists in the table.
|
||||
|
||||
Args:
|
||||
material_name: Material name
|
||||
|
||||
Returns:
|
||||
True if material exists, False otherwise
|
||||
"""
|
||||
sql = """
|
||||
SELECT COUNT(*) as count
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [MaterialName] = ?
|
||||
"""
|
||||
with get_connection() as db:
|
||||
result = db.execute_query(sql, (material_name,))
|
||||
return result[0]['count'] > 0 if result else False
|
||||
|
||||
def count_by_manager(self, manager_name: str) -> int:
|
||||
"""
|
||||
Count materials for a specific manager.
|
||||
|
||||
Args:
|
||||
manager_name: Manager name
|
||||
|
||||
Returns:
|
||||
Number of materials for the manager
|
||||
"""
|
||||
sql = """
|
||||
SELECT COUNT(*) as count
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [ManagerName] = ?
|
||||
"""
|
||||
with get_connection() as db:
|
||||
result = db.execute_query(sql, (manager_name,))
|
||||
return result[0]['count'] if result else 0
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get comprehensive statistics about the data.
|
||||
|
||||
Returns:
|
||||
Dictionary with statistics including total materials,
|
||||
unique managers, and materials per manager
|
||||
"""
|
||||
sql = """
|
||||
SELECT
|
||||
COUNT(*) as total_materials,
|
||||
COUNT(DISTINCT ManagerName) as unique_managers
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [MaterialName] IS NOT NULL
|
||||
"""
|
||||
with get_connection() as db:
|
||||
result = db.execute_query(sql)
|
||||
stats = result[0] if result else {}
|
||||
|
||||
# Get materials per manager
|
||||
manager_sql = """
|
||||
SELECT [ManagerName], COUNT(*) as count
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [ManagerName] IS NOT NULL
|
||||
GROUP BY [ManagerName]
|
||||
ORDER BY count DESC
|
||||
"""
|
||||
manager_results = db.execute_query(manager_sql)
|
||||
stats['materials_per_manager'] = [
|
||||
{r['ManagerName']: r['count']} for r in manager_results
|
||||
]
|
||||
|
||||
return stats
|
||||
|
||||
def search_materials(self, keyword: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Search materials by keyword (partial match).
|
||||
|
||||
Args:
|
||||
keyword: Keyword to search for
|
||||
|
||||
Returns:
|
||||
List of matching materials
|
||||
"""
|
||||
sql = """
|
||||
SELECT [MaterialName], [ManagerName]
|
||||
FROM [dbo].[MaterialsToBeDeleted]
|
||||
WHERE [MaterialName] LIKE ?
|
||||
ORDER BY [ManagerName], [MaterialName]
|
||||
"""
|
||||
with get_connection() as db:
|
||||
return db.execute_query(sql, (f'%{keyword}%',))
|
||||
99
db/production_contract_data_dao.py
Normal file
99
db/production_contract_data_dao.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Data Access Object for production contract data.
|
||||
|
||||
This module provides query operations for accessing production contract data
|
||||
from the [productionContractData].[26年压力表合同数据] table.
|
||||
"""
|
||||
|
||||
from typing import List, Dict, Any
|
||||
from db.connection import get_connection
|
||||
|
||||
|
||||
class ProductionContractDataDAO:
|
||||
"""Data Access Object for production contract data queries"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = None
|
||||
|
||||
def __enter__(self):
|
||||
"""Enter context manager and establish database connection"""
|
||||
self.db = get_connection()
|
||||
self.db.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Exit context manager and close database connection"""
|
||||
if self.db:
|
||||
self.db.disconnect()
|
||||
|
||||
def close(self):
|
||||
"""Close database connection"""
|
||||
if self.db:
|
||||
self.db.disconnect()
|
||||
|
||||
def query_by_总排号(self, 总排号_list: List[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Query production contract data by 总排号 list.
|
||||
|
||||
Args:
|
||||
总排号_list: List of 总排号 values to query
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing 总排号, 生产订单号, 序号, 订单号, 客户名称, 产品型号
|
||||
"""
|
||||
if not 总排号_list:
|
||||
return []
|
||||
|
||||
# SQL Server parameter limit requires batching
|
||||
batch_size = 2000
|
||||
all_results = []
|
||||
|
||||
for i in range(0, len(总排号_list), batch_size):
|
||||
batch = 总排号_list[i:i + batch_size]
|
||||
placeholders = ','.join(['?' for _ in batch])
|
||||
sql = f"""
|
||||
SELECT [总排号], [生产订单号], [序号], [订单号], [客户名称], [产品型号]
|
||||
FROM [productionContractData].[26年压力表合同数据]
|
||||
WHERE [总排号] IN ({placeholders})
|
||||
ORDER BY [序号]
|
||||
"""
|
||||
|
||||
with get_connection() as db:
|
||||
results = db.execute_query(sql, tuple(batch))
|
||||
all_results.extend(results)
|
||||
|
||||
return all_results
|
||||
|
||||
def get_source_numbers_by_总排号(self, 总排号_list: List[str]) -> List[str]:
|
||||
"""
|
||||
Extract unique 生产订单号 values by 总排号 list.
|
||||
|
||||
Args:
|
||||
总排号_list: List of 总排号 values to query
|
||||
|
||||
Returns:
|
||||
List of unique 生产订单号 values (SourceNumber)
|
||||
"""
|
||||
results = self.query_by_总排号(总排号_list)
|
||||
# Extract unique 生产订单号 values, excluding None/null values
|
||||
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]:
|
||||
"""
|
||||
Get mapping between 总排号 and 生产订单号.
|
||||
|
||||
Args:
|
||||
总排号_list: List of 总排号 values to query
|
||||
|
||||
Returns:
|
||||
Dictionary mapping 总排号 -> 生产订单号
|
||||
"""
|
||||
results = self.query_by_总排号(总排号_list)
|
||||
return {
|
||||
r['总排号']: r['生产订单号']
|
||||
for r in results
|
||||
if r.get('总排号') and r.get('生产订单号')
|
||||
}
|
||||
@@ -4,12 +4,17 @@
|
||||
物料校验标签页
|
||||
|
||||
校验物料状态并匹配待删除物料。
|
||||
支持 4 种校验模式:
|
||||
1. database_full - 数据库全表校验
|
||||
2. database_filtered - 数据库 ProductionID 过滤校验
|
||||
3. excel_existing - Excel 现有文件校验
|
||||
4. excel_full - Excel 完整工作流
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
from pathlib import Path
|
||||
from io import StringIO
|
||||
from contextlib import redirect_stdout
|
||||
@@ -64,58 +69,95 @@ class MaterialValidationTab(ttk.Frame):
|
||||
self._create_result_table(result_frame)
|
||||
self._create_log_panel(log_frame)
|
||||
|
||||
# 初始化文件选择器显示状态
|
||||
self._on_source_mode_change()
|
||||
|
||||
def _create_control_panel(self, parent):
|
||||
"""创建控制面板"""
|
||||
# 数据来源选择
|
||||
source_group = ttk.LabelFrame(parent, text="数据来源", padding=10)
|
||||
source_group.pack(fill=tk.X, pady=5)
|
||||
|
||||
self.source_mode = tk.StringVar(value="existing")
|
||||
self.source_mode = tk.StringVar(value="database_full")
|
||||
|
||||
# 数据库模式
|
||||
ttk.Radiobutton(
|
||||
source_group,
|
||||
text="使用现有 Excel 文件",
|
||||
text="数据库 - 全表校验",
|
||||
variable=self.source_mode,
|
||||
value="existing",
|
||||
value="database_full",
|
||||
command=self._on_source_mode_change,
|
||||
).grid(row=0, column=0, sticky="w", padx=5)
|
||||
|
||||
ttk.Radiobutton(
|
||||
source_group,
|
||||
text="完整工作流 (提取 + 校验)",
|
||||
text="数据库 - ProductionID 过滤",
|
||||
variable=self.source_mode,
|
||||
value="full",
|
||||
value="database_filtered",
|
||||
command=self._on_source_mode_change,
|
||||
).grid(row=0, column=1, sticky="w", padx=5)
|
||||
|
||||
# Excel 模式
|
||||
ttk.Radiobutton(
|
||||
source_group,
|
||||
text="Excel - 现有文件",
|
||||
variable=self.source_mode,
|
||||
value="excel_existing",
|
||||
command=self._on_source_mode_change,
|
||||
).grid(row=1, column=0, sticky="w", padx=5)
|
||||
|
||||
ttk.Radiobutton(
|
||||
source_group,
|
||||
text="Excel - 完整工作流",
|
||||
variable=self.source_mode,
|
||||
value="excel_full",
|
||||
command=self._on_source_mode_change,
|
||||
).grid(row=1, column=1, sticky="w", padx=5)
|
||||
|
||||
# 文件选择
|
||||
file_group = ttk.LabelFrame(parent, text="文件选择", padding=10)
|
||||
file_group.pack(fill=tk.X, pady=5)
|
||||
|
||||
# 现有 Excel 文件
|
||||
self.existing_excel_frame = ttk.Frame(file_group)
|
||||
self.existing_excel_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||
# 数据库全表模式 - 无需输入文件
|
||||
self.db_full_frame = ttk.Frame(file_group)
|
||||
ttk.Label(
|
||||
self.db_full_frame,
|
||||
text="数据库全表模式:将查询 DiscreteMaterialPlanData 表中的所有材料",
|
||||
foreground="gray"
|
||||
).pack(anchor="w")
|
||||
|
||||
self.existing_excel_selector = FileSelector(
|
||||
self.existing_excel_frame,
|
||||
label_text="现有 Excel 文件:",
|
||||
file_type="file",
|
||||
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
||||
initial_dir=self.config.get("paths.data_dir", "data/"),
|
||||
)
|
||||
self.existing_excel_selector.pack(fill=tk.X)
|
||||
|
||||
# ProductionID 文件 (完整工作流模式)
|
||||
self.production_id_frame = ttk.Frame(file_group)
|
||||
# 初始隐藏
|
||||
|
||||
self.production_id_selector = FileSelector(
|
||||
self.production_id_frame,
|
||||
# 数据库过滤模式 - 需要 ProductionID 文件
|
||||
self.db_filtered_frame = ttk.Frame(file_group)
|
||||
self.db_filtered_production_id_selector = FileSelector(
|
||||
self.db_filtered_frame,
|
||||
label_text="ProductionID 文件:",
|
||||
file_type="file",
|
||||
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
||||
initial_dir="D:/python/playwrite/",
|
||||
)
|
||||
self.production_id_selector.pack(fill=tk.X)
|
||||
self.db_filtered_production_id_selector.pack(fill=tk.X)
|
||||
|
||||
# Excel 现有文件模式
|
||||
self.excel_existing_frame = ttk.Frame(file_group)
|
||||
self.excel_existing_selector = FileSelector(
|
||||
self.excel_existing_frame,
|
||||
label_text="现有 Excel 文件:",
|
||||
file_type="file",
|
||||
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
||||
initial_dir=self.config.get("paths.data_dir", "data/"),
|
||||
)
|
||||
self.excel_existing_selector.pack(fill=tk.X)
|
||||
|
||||
# Excel 完整工作流模式
|
||||
self.excel_full_frame = ttk.Frame(file_group)
|
||||
self.excel_full_production_id_selector = FileSelector(
|
||||
self.excel_full_frame,
|
||||
label_text="ProductionID 文件:",
|
||||
file_type="file",
|
||||
file_types=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
||||
initial_dir="D:/python/playwrite/",
|
||||
)
|
||||
self.excel_full_production_id_selector.pack(fill=tk.X)
|
||||
|
||||
# 输出文件
|
||||
output_frame = ttk.Frame(file_group)
|
||||
@@ -196,12 +238,23 @@ class MaterialValidationTab(ttk.Frame):
|
||||
|
||||
def _on_source_mode_change(self):
|
||||
"""数据源模式切换"""
|
||||
if self.source_mode.get() == "existing":
|
||||
self.existing_excel_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||
self.production_id_frame.grid_remove()
|
||||
else:
|
||||
self.existing_excel_frame.grid_remove()
|
||||
self.production_id_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||
mode = self.source_mode.get()
|
||||
|
||||
# 隐藏所有文件选择框架
|
||||
self.db_full_frame.grid_remove()
|
||||
self.db_filtered_frame.grid_remove()
|
||||
self.excel_existing_frame.grid_remove()
|
||||
self.excel_full_frame.grid_remove()
|
||||
|
||||
# 根据模式显示对应的文件选择器
|
||||
if mode == "database_full":
|
||||
self.db_full_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||
elif mode == "database_filtered":
|
||||
self.db_filtered_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||
elif mode == "excel_existing":
|
||||
self.excel_existing_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||
elif mode == "excel_full":
|
||||
self.excel_full_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||||
|
||||
def start_validation(self):
|
||||
"""开始校验"""
|
||||
@@ -211,25 +264,38 @@ class MaterialValidationTab(ttk.Frame):
|
||||
messagebox.showerror("错误", "请指定输出文件路径")
|
||||
return
|
||||
|
||||
# 获取输入文件
|
||||
if self.source_mode.get() == "existing":
|
||||
input_file = self.existing_excel_selector.get()
|
||||
if not input_file:
|
||||
messagebox.showerror("错误", "请选择现有 Excel 文件")
|
||||
return
|
||||
if not os.path.exists(input_file):
|
||||
messagebox.showerror("错误", f"文件不存在:{input_file}")
|
||||
return
|
||||
production_id_file = None
|
||||
else:
|
||||
production_id_file = self.production_id_selector.get()
|
||||
mode = self.source_mode.get()
|
||||
input_file = None
|
||||
production_id_file = None
|
||||
|
||||
# 根据模式验证输入文件
|
||||
if mode == "database_full":
|
||||
# 无需输入文件
|
||||
pass
|
||||
elif mode == "database_filtered":
|
||||
production_id_file = self.db_filtered_production_id_selector.get()
|
||||
if not production_id_file:
|
||||
messagebox.showerror("错误", "请选择 ProductionID 文件")
|
||||
return
|
||||
if not os.path.exists(production_id_file):
|
||||
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
|
||||
return
|
||||
elif mode == "excel_existing":
|
||||
input_file = self.excel_existing_selector.get()
|
||||
if not input_file:
|
||||
messagebox.showerror("错误", "请选择现有 Excel 文件")
|
||||
return
|
||||
if not os.path.exists(input_file):
|
||||
messagebox.showerror("错误", f"文件不存在:{input_file}")
|
||||
return
|
||||
elif mode == "excel_full":
|
||||
production_id_file = self.excel_full_production_id_selector.get()
|
||||
if not production_id_file:
|
||||
messagebox.showerror("错误", "请选择 ProductionID 文件")
|
||||
return
|
||||
if not os.path.exists(production_id_file):
|
||||
messagebox.showerror("错误", f"文件不存在:{production_id_file}")
|
||||
return
|
||||
input_file = None
|
||||
|
||||
# 确保输出目录存在
|
||||
output_dir = os.path.dirname(output_file)
|
||||
@@ -240,7 +306,7 @@ class MaterialValidationTab(ttk.Frame):
|
||||
self.validating = True
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.log_text.clear()
|
||||
self.log_text.info("开始物料校验...")
|
||||
self.log_text.info(f"开始物料校验(模式: {mode})...")
|
||||
|
||||
# 清空结果表格
|
||||
for item in self.tree.get_children():
|
||||
@@ -249,13 +315,13 @@ class MaterialValidationTab(ttk.Frame):
|
||||
# 在后台线程中执行校验
|
||||
validation_thread = threading.Thread(
|
||||
target=self._validation_worker,
|
||||
args=(input_file, production_id_file, output_file),
|
||||
args=(mode, input_file, production_id_file, output_file),
|
||||
daemon=True,
|
||||
)
|
||||
validation_thread.start()
|
||||
|
||||
def _validation_worker(
|
||||
self, input_file: str, production_id_file: str, output_file: str
|
||||
self, mode: str, input_file: str, production_id_file: str, output_file: str
|
||||
):
|
||||
"""校验工作线程"""
|
||||
try:
|
||||
@@ -273,18 +339,32 @@ class MaterialValidationTab(ttk.Frame):
|
||||
# 捕获 stdout 输出
|
||||
captured_output = StringIO()
|
||||
|
||||
# 执行校验并捕获输出
|
||||
# 根据模式执行校验
|
||||
with redirect_stdout(captured_output):
|
||||
if self.source_mode.get() == "existing":
|
||||
result = validator.validate_from_existing_excel(
|
||||
excel_file=input_file, output_file=output_file
|
||||
if mode == "database_full":
|
||||
result = validator.validate_from_database(
|
||||
full_table=True,
|
||||
output_file=output_file
|
||||
)
|
||||
else:
|
||||
elif mode == "database_filtered":
|
||||
result = validator.validate_from_database(
|
||||
production_id_file=production_id_file,
|
||||
full_table=False,
|
||||
output_file=output_file
|
||||
)
|
||||
elif mode == "excel_existing":
|
||||
result = validator.validate_from_existing_excel(
|
||||
excel_file=input_file,
|
||||
output_file=output_file
|
||||
)
|
||||
elif mode == "excel_full":
|
||||
result = validator.validate(
|
||||
production_id_file=production_id_file,
|
||||
merged_excel_file=None, # 将在内部生成
|
||||
merged_excel_file=None,
|
||||
output_file=output_file,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"未知的校验模式: {mode}")
|
||||
|
||||
# 获取捕获的输出并显示到日志
|
||||
output_text = captured_output.getvalue()
|
||||
|
||||
@@ -57,9 +57,12 @@ class SettingsTab(ttk.Frame):
|
||||
# 处理配置组
|
||||
self._create_extraction_group(scrollable_frame)
|
||||
|
||||
# 校验配置组
|
||||
self._create_validation_group(scrollable_frame)
|
||||
|
||||
# 按钮区域
|
||||
button_frame = ttk.Frame(scrollable_frame)
|
||||
button_frame.grid(row=5, column=0, columnspan=2, pady=20, sticky="ew")
|
||||
button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew")
|
||||
|
||||
ttk.Button(
|
||||
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
|
||||
@@ -224,6 +227,63 @@ class SettingsTab(ttk.Frame):
|
||||
group, text="保存到数据库 (同时写入 SQL Server)", variable=self.enable_db_persistence_var
|
||||
).grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
|
||||
|
||||
def _create_validation_group(self, parent):
|
||||
"""创建物料校验配置组"""
|
||||
group = ttk.LabelFrame(parent, text="物料校验设置", padding=10)
|
||||
group.grid(row=4, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
|
||||
|
||||
# 数据源选择
|
||||
ttk.Label(group, text="默认数据源:").grid(row=0, column=0, sticky="w", pady=5)
|
||||
self.validation_data_source_var = tk.StringVar()
|
||||
data_source_combo = ttk.Combobox(
|
||||
group,
|
||||
textvariable=self.validation_data_source_var,
|
||||
values=["database_full", "database_filtered", "excel_existing", "excel_full"],
|
||||
state="readonly",
|
||||
width=30,
|
||||
)
|
||||
data_source_combo.grid(row=0, column=1, sticky="w", pady=5)
|
||||
|
||||
# 使用数据库
|
||||
self.validation_use_database_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(
|
||||
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)
|
||||
self.validation_batch_size_var = tk.IntVar(value=2000)
|
||||
ttk.Spinbox(
|
||||
group, from_=100, to=2000, textvariable=self.validation_batch_size_var, width=10
|
||||
).grid(row=2, column=1, sticky="w", pady=5)
|
||||
|
||||
# 匹配模式
|
||||
ttk.Label(group, text="匹配模式:").grid(row=3, column=0, sticky="w", pady=5)
|
||||
self.validation_match_mode_var = tk.StringVar()
|
||||
match_mode_combo = ttk.Combobox(
|
||||
group,
|
||||
textvariable=self.validation_match_mode_var,
|
||||
values=["substring", "exact"],
|
||||
state="readonly",
|
||||
width=30,
|
||||
)
|
||||
match_mode_combo.grid(row=3, column=1, sticky="w", pady=5)
|
||||
|
||||
# CRUD 操作
|
||||
self.validation_enable_crud_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(
|
||||
group, text="启用 CRUD 操作(管理待删除物料)", variable=self.validation_enable_crud_var
|
||||
).grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
|
||||
|
||||
# 默认负责人
|
||||
ttk.Label(group, text="默认负责人:").grid(row=5, 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=5, column=1, sticky="w", pady=5
|
||||
)
|
||||
|
||||
group.columnconfigure(1, weight=1)
|
||||
|
||||
def load_settings(self):
|
||||
"""从配置加载设置到界面"""
|
||||
# ERP 设置
|
||||
@@ -255,6 +315,14 @@ class SettingsTab(ttk.Frame):
|
||||
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.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_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", ""))
|
||||
|
||||
def save_settings(self):
|
||||
"""保存界面设置到配置"""
|
||||
# ERP 设置
|
||||
@@ -284,6 +352,14 @@ class SettingsTab(ttk.Frame):
|
||||
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("validation.data_source", self.validation_data_source_var.get())
|
||||
self.config.set("validation.use_database", self.validation_use_database_var.get())
|
||||
self.config.set("validation.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())
|
||||
|
||||
# 保存到文件
|
||||
if self.config.save():
|
||||
messagebox.showinfo("成功", "设置已保存")
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""
|
||||
物料状态校验工具
|
||||
校验订单中的物料状态,匹配待删除物料
|
||||
|
||||
支持两种数据源:
|
||||
1. Excel 文件(原有方式)
|
||||
2. 数据库驱动(新增方式)
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -8,6 +12,9 @@ import pandas as pd
|
||||
from typing import List, Dict, Any
|
||||
from utils.离散备料计划维护数据提取 import DiscreteMaterialPlanExtractor
|
||||
from db.materials_to_delete import get_all_materials_to_delete
|
||||
from db.production_contract_data_dao import ProductionContractDataDAO
|
||||
from db.discrete_material_plan_dao import DiscreteMaterialPlanDAO
|
||||
from db.materials_to_be_deleted_dao import MaterialsToBeDeletedDAO
|
||||
|
||||
|
||||
class MaterialStatusValidator:
|
||||
@@ -195,3 +202,147 @@ class MaterialStatusValidator:
|
||||
self._print(f" 未匹配: {len(results) - matched_count}")
|
||||
|
||||
return output_file
|
||||
|
||||
# ==================== DATABASE-DRIVEN VALIDATION ====================
|
||||
|
||||
def _read_production_ids(self, production_id_file: str) -> List[str]:
|
||||
"""
|
||||
读取 ProductionID.txt 文件
|
||||
|
||||
Args:
|
||||
production_id_file: ProductionID.txt 文件路径
|
||||
|
||||
Returns:
|
||||
List[str]: 总排号列表
|
||||
"""
|
||||
with open(production_id_file, 'r', encoding='utf-8') as f:
|
||||
production_ids = [line.strip() for line in f if line.strip()]
|
||||
return production_ids
|
||||
|
||||
def _get_source_numbers_from_production_ids(
|
||||
self, production_ids: List[str]
|
||||
) -> List[str]:
|
||||
"""
|
||||
通过 ProductionID 查询获取 SourceNumber 列表
|
||||
|
||||
查询链路:
|
||||
ProductionID (总排号) -> productionContractData.26年压力表合同数据.生产订单号
|
||||
|
||||
Args:
|
||||
production_ids: 总排号列表
|
||||
|
||||
Returns:
|
||||
List[str]: 生产订单号列表
|
||||
"""
|
||||
self._print(f"[INFO] 正在查询 {len(production_ids)} 个总排号对应的生产订单号...")
|
||||
|
||||
contract_dao = ProductionContractDataDAO()
|
||||
source_numbers = contract_dao.get_source_numbers_by_总排号(production_ids)
|
||||
|
||||
self._print(f"[INFO] 找到 {len(source_numbers)} 个唯一的生产订单号")
|
||||
return source_numbers
|
||||
|
||||
def _get_material_names_from_db(
|
||||
self, source_numbers: List[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
从数据库获取材料名称,可选按 SourceNumber 过滤
|
||||
|
||||
Args:
|
||||
source_numbers: 可选的 SourceNumber 列表进行过滤
|
||||
|
||||
Returns:
|
||||
List[str]: 唯一材料名称列表
|
||||
"""
|
||||
if source_numbers is None or not source_numbers:
|
||||
self._print("[INFO] 查询所有材料的名称...")
|
||||
else:
|
||||
self._print(f"[INFO] 查询 {len(source_numbers)} 个生产订单对应的材料名称...")
|
||||
|
||||
dao = DiscreteMaterialPlanDAO()
|
||||
material_names = dao.get_unique_material_names(source_numbers)
|
||||
|
||||
self._print(f"[INFO] 找到 {len(material_names)} 个唯一材料名称")
|
||||
return material_names
|
||||
|
||||
def validate_from_database(
|
||||
self,
|
||||
production_id_file: str = None,
|
||||
full_table: bool = False,
|
||||
output_file: str = None
|
||||
) -> str:
|
||||
"""
|
||||
使用数据库作为数据源执行校验
|
||||
|
||||
支持两种模式:
|
||||
1. 全表校验 (full_table=True): 查询整个 DiscreteMaterialPlanData 表
|
||||
2. ProductionID 过滤校验 (production_id_file 指定): 基于 ProductionID.txt 文件过滤
|
||||
|
||||
查询链路(模式2):
|
||||
ProductionID.txt (总排号)
|
||||
-> productionContractData.26年压力表合同数据.生产订单号 (SourceNumber)
|
||||
-> DiscreteMaterialPlanData.SourceNumber
|
||||
-> DiscreteMaterialPlanData.MaterialName
|
||||
-> 对比 MaterialsToBeDeleted.MaterialName
|
||||
|
||||
Args:
|
||||
production_id_file: ProductionID.txt 路径(模式2)
|
||||
full_table: 是否全表校验(模式1)
|
||||
output_file: 输出文件路径
|
||||
|
||||
Returns:
|
||||
输出文件路径
|
||||
"""
|
||||
# 设置默认输出路径
|
||||
if output_file is None:
|
||||
output_file = "D:/python/playwrite/data/物料状态校验结果.xlsx"
|
||||
|
||||
self._print("=" * 60)
|
||||
self._print("使用数据库数据源执行校验...")
|
||||
|
||||
# 根据模式获取材料名称
|
||||
if full_table:
|
||||
self._print("\n模式: 全表校验")
|
||||
self._print("[INFO] 查询 DiscreteMaterialPlanData 表中的所有材料...")
|
||||
material_names = self._get_material_names_from_db(None)
|
||||
elif production_id_file:
|
||||
self._print("\n模式: ProductionID 过滤校验")
|
||||
self._print(f"[INFO] 读取 ProductionID 文件: {production_id_file}")
|
||||
|
||||
# 1. 读取 ProductionID.txt
|
||||
production_ids = self._read_production_ids(production_id_file)
|
||||
self._print(f"[INFO] 读取到 {len(production_ids)} 个总排号")
|
||||
|
||||
# 2. 查询获取 SourceNumbers
|
||||
source_numbers = self._get_source_numbers_from_production_ids(production_ids)
|
||||
|
||||
# 3. 获取材料名称
|
||||
material_names = self._get_material_names_from_db(source_numbers)
|
||||
else:
|
||||
raise ValueError(
|
||||
"必须指定 full_table=True 或提供 production_id_file 参数"
|
||||
)
|
||||
|
||||
# 从数据库获取待删除物料
|
||||
self._print("\n从数据库获取待删除物料...")
|
||||
db_materials = get_all_materials_to_delete()
|
||||
self._print(f"获取到 {len(db_materials)} 条待删除物料记录")
|
||||
|
||||
# 匹配物料
|
||||
self._print("\n匹配物料...")
|
||||
results = self.match_materials(material_names, db_materials)
|
||||
|
||||
# 输出结果
|
||||
self._print("\n输出结果...")
|
||||
result_df = pd.DataFrame(results)
|
||||
result_df.to_excel(output_file, index=False)
|
||||
self._print(f"结果已保存: {output_file}")
|
||||
|
||||
# 打印统计信息
|
||||
matched_count = sum(1 for r in results if r["匹配状态"] == "匹配成功")
|
||||
self._print(f"\n统计信息:")
|
||||
self._print(f" 总材料数: {len(results)}")
|
||||
self._print(f" 匹配成功: {matched_count}")
|
||||
self._print(f" 未匹配: {len(results) - matched_count}")
|
||||
|
||||
return output_file
|
||||
|
||||
Reference in New Issue
Block a user