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:
Misaka_Company
2026-02-06 13:08:31 +08:00
parent 53a1e33e45
commit 2096a51f55
9 changed files with 976 additions and 57 deletions

View File

@@ -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)

View 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}%',))

View 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('生产订单号')
}