- Add SchemaVerifier class for automatic database schema verification and repair - Implement warehouse_manager (Level 75) and warehouse_worker (Level 35) roles - Add zone-based access control for warehouse workers - Implement worker-manager binding system with zone filtering - Add comprehensive database auto-repair on Docker initialization - Remove Module Access section from user form (role-based access only) - Add autocomplete attributes to password fields for better UX - Include detailed documentation for warehouse implementation - Update initialize_db.py with schema verification as Step 0
330 lines
9.8 KiB
Python
330 lines
9.8 KiB
Python
"""
|
|
Warehouse Worker Management Functions
|
|
Handles worker-manager bindings and zone assignments for the warehouse module
|
|
"""
|
|
from app.database import get_db
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def assign_worker_to_manager(manager_id, worker_id, warehouse_zone=None):
|
|
"""
|
|
Assign a warehouse worker to a manager with optional zone restriction
|
|
|
|
Args:
|
|
manager_id (int): Manager user ID
|
|
worker_id (int): Worker user ID
|
|
warehouse_zone (str, optional): Zone restriction (e.g., "Cold Storage", "High Shelf")
|
|
|
|
Returns:
|
|
tuple: (success: bool, message: str)
|
|
"""
|
|
try:
|
|
if manager_id == worker_id:
|
|
return False, "Cannot assign a worker to themselves"
|
|
|
|
db = get_db()
|
|
cursor = db.cursor()
|
|
|
|
# Check if binding already exists
|
|
cursor.execute("""
|
|
SELECT id FROM worker_manager_bindings
|
|
WHERE manager_id = %s AND worker_id = %s
|
|
""", (manager_id, worker_id))
|
|
|
|
existing = cursor.fetchone()
|
|
|
|
if existing:
|
|
# Update existing binding
|
|
cursor.execute("""
|
|
UPDATE worker_manager_bindings
|
|
SET warehouse_zone = %s, is_active = 1, updated_at = NOW()
|
|
WHERE manager_id = %s AND worker_id = %s
|
|
""", (warehouse_zone, manager_id, worker_id))
|
|
message = f"Worker assignment updated (zone: {warehouse_zone or 'all zones'})"
|
|
else:
|
|
# Create new binding
|
|
cursor.execute("""
|
|
INSERT INTO worker_manager_bindings (manager_id, worker_id, warehouse_zone)
|
|
VALUES (%s, %s, %s)
|
|
""", (manager_id, worker_id, warehouse_zone))
|
|
message = f"Worker assigned to manager (zone: {warehouse_zone or 'all zones'})"
|
|
|
|
db.commit()
|
|
logger.info(f"✓ {message} - Manager ID: {manager_id}, Worker ID: {worker_id}")
|
|
return True, message
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error assigning worker to manager: {e}")
|
|
return False, f"Error: {str(e)}"
|
|
|
|
|
|
def unassign_worker_from_manager(manager_id, worker_id, soft_delete=True):
|
|
"""
|
|
Remove a worker from a manager's supervision
|
|
|
|
Args:
|
|
manager_id (int): Manager user ID
|
|
worker_id (int): Worker user ID
|
|
soft_delete (bool): If True, set is_active=0; if False, delete record
|
|
|
|
Returns:
|
|
tuple: (success: bool, message: str)
|
|
"""
|
|
try:
|
|
db = get_db()
|
|
cursor = db.cursor()
|
|
|
|
if soft_delete:
|
|
cursor.execute("""
|
|
UPDATE worker_manager_bindings
|
|
SET is_active = 0, updated_at = NOW()
|
|
WHERE manager_id = %s AND worker_id = %s
|
|
""", (manager_id, worker_id))
|
|
else:
|
|
cursor.execute("""
|
|
DELETE FROM worker_manager_bindings
|
|
WHERE manager_id = %s AND worker_id = %s
|
|
""", (manager_id, worker_id))
|
|
|
|
db.commit()
|
|
logger.info(f"✓ Worker unassigned - Manager ID: {manager_id}, Worker ID: {worker_id}")
|
|
return True, "Worker unassigned successfully"
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error unassigning worker: {e}")
|
|
return False, f"Error: {str(e)}"
|
|
|
|
|
|
def get_worker_binding_info(worker_id):
|
|
"""
|
|
Get binding information for a worker (who manages them, what zone)
|
|
|
|
Args:
|
|
worker_id (int): Worker user ID
|
|
|
|
Returns:
|
|
dict: {manager_id, manager_username, manager_name, zone, is_active} or None
|
|
"""
|
|
try:
|
|
db = get_db()
|
|
cursor = db.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT m.id, m.username, m.full_name, wmb.warehouse_zone, wmb.is_active
|
|
FROM worker_manager_bindings wmb
|
|
JOIN users m ON wmb.manager_id = m.id
|
|
WHERE wmb.worker_id = %s
|
|
LIMIT 1
|
|
""", (worker_id,))
|
|
|
|
result = cursor.fetchone()
|
|
if result:
|
|
return {
|
|
'manager_id': result[0],
|
|
'manager_username': result[1],
|
|
'manager_name': result[2],
|
|
'zone': result[3],
|
|
'is_active': result[4]
|
|
}
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting worker binding info: {e}")
|
|
return None
|
|
|
|
|
|
def get_available_workers_for_assignment(exclude_manager_id=None):
|
|
"""
|
|
Get all warehouse_worker users available for assignment
|
|
|
|
Args:
|
|
exclude_manager_id (int, optional): Manager ID to exclude from results
|
|
|
|
Returns:
|
|
list: List of dicts {id, username, full_name, current_manager}
|
|
"""
|
|
try:
|
|
db = get_db()
|
|
cursor = db.cursor()
|
|
|
|
query = """
|
|
SELECT u.id, u.username, u.full_name,
|
|
(SELECT m.full_name FROM worker_manager_bindings wmb
|
|
JOIN users m ON wmb.manager_id = m.id
|
|
WHERE wmb.worker_id = u.id AND wmb.is_active = 1 LIMIT 1) as current_manager
|
|
FROM users u
|
|
WHERE u.role = 'warehouse_worker'
|
|
ORDER BY u.full_name
|
|
"""
|
|
|
|
cursor.execute(query)
|
|
|
|
results = []
|
|
for row in cursor.fetchall():
|
|
results.append({
|
|
'id': row[0],
|
|
'username': row[1],
|
|
'full_name': row[2],
|
|
'current_manager': row[3]
|
|
})
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting available workers: {e}")
|
|
return []
|
|
|
|
|
|
def get_manager_assigned_workers(manager_id, include_inactive=False):
|
|
"""
|
|
Get all workers assigned to a specific manager
|
|
|
|
Args:
|
|
manager_id (int): Manager user ID
|
|
include_inactive (bool): Whether to include inactive bindings
|
|
|
|
Returns:
|
|
list: List of dicts {id, username, full_name, zone, is_active}
|
|
"""
|
|
try:
|
|
db = get_db()
|
|
cursor = db.cursor()
|
|
|
|
query = """
|
|
SELECT u.id, u.username, u.full_name, wmb.warehouse_zone, wmb.is_active
|
|
FROM worker_manager_bindings wmb
|
|
JOIN users u ON wmb.worker_id = u.id
|
|
WHERE wmb.manager_id = %s
|
|
"""
|
|
|
|
params = [manager_id]
|
|
|
|
if not include_inactive:
|
|
query += " AND wmb.is_active = 1"
|
|
|
|
query += " ORDER BY u.full_name"
|
|
|
|
cursor.execute(query, params)
|
|
|
|
results = []
|
|
for row in cursor.fetchall():
|
|
results.append({
|
|
'id': row[0],
|
|
'username': row[1],
|
|
'full_name': row[2],
|
|
'zone': row[3],
|
|
'is_active': row[4]
|
|
})
|
|
|
|
return results
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting manager's workers: {e}")
|
|
return []
|
|
|
|
|
|
def validate_zone_name(zone_name):
|
|
"""
|
|
Validate zone name format (alphanumeric, spaces, hyphens, underscores allowed)
|
|
|
|
Args:
|
|
zone_name (str): Zone name to validate
|
|
|
|
Returns:
|
|
tuple: (is_valid: bool, message: str)
|
|
"""
|
|
if not zone_name:
|
|
return True, "No zone restriction" # NULL zone is valid (all zones)
|
|
|
|
if not isinstance(zone_name, str):
|
|
return False, "Zone must be a string"
|
|
|
|
zone_name = zone_name.strip()
|
|
|
|
if len(zone_name) > 100:
|
|
return False, "Zone name too long (max 100 characters)"
|
|
|
|
if len(zone_name) < 2:
|
|
return False, "Zone name too short (min 2 characters)"
|
|
|
|
# Allow alphanumeric, spaces, hyphens, underscores
|
|
import re
|
|
if not re.match(r'^[a-zA-Z0-9\s\-_]+$', zone_name):
|
|
return False, "Zone name contains invalid characters"
|
|
|
|
return True, "Valid zone name"
|
|
|
|
|
|
def get_warehouse_zones():
|
|
"""
|
|
Get list of all warehouse zones in use
|
|
|
|
Returns:
|
|
list: List of zone names currently assigned to workers
|
|
"""
|
|
try:
|
|
db = get_db()
|
|
cursor = db.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT DISTINCT warehouse_zone
|
|
FROM worker_manager_bindings
|
|
WHERE is_active = 1 AND warehouse_zone IS NOT NULL
|
|
ORDER BY warehouse_zone
|
|
""")
|
|
|
|
zones = [row[0] for row in cursor.fetchall()]
|
|
return zones
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting warehouse zones: {e}")
|
|
return []
|
|
|
|
|
|
def reassign_worker(worker_id, new_manager_id, warehouse_zone=None):
|
|
"""
|
|
Reassign a worker from current manager to a new manager
|
|
|
|
Args:
|
|
worker_id (int): Worker user ID
|
|
new_manager_id (int): New manager user ID
|
|
warehouse_zone (str, optional): New zone restriction
|
|
|
|
Returns:
|
|
tuple: (success: bool, message: str)
|
|
"""
|
|
try:
|
|
if worker_id == new_manager_id:
|
|
return False, "Cannot assign a worker to themselves"
|
|
|
|
db = get_db()
|
|
cursor = db.cursor()
|
|
|
|
# Deactivate old binding
|
|
cursor.execute("""
|
|
UPDATE worker_manager_bindings
|
|
SET is_active = 0
|
|
WHERE worker_id = %s AND is_active = 1
|
|
""", (worker_id,))
|
|
|
|
# Create new binding
|
|
cursor.execute("""
|
|
INSERT INTO worker_manager_bindings (manager_id, worker_id, warehouse_zone)
|
|
VALUES (%s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE
|
|
is_active = 1,
|
|
warehouse_zone = %s,
|
|
updated_at = NOW()
|
|
""", (new_manager_id, worker_id, warehouse_zone, warehouse_zone))
|
|
|
|
db.commit()
|
|
|
|
logger.info(f"✓ Worker reassigned - Worker ID: {worker_id}, New Manager ID: {new_manager_id}")
|
|
return True, "Worker reassigned successfully"
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error reassigning worker: {e}")
|
|
return False, f"Error: {str(e)}"
|