- Add boxes_crates database table with BIGINT IDs and 8-digit auto-numbered box_numbers - Implement boxes CRUD operations (add, edit, update, delete, delete_multiple) - Create boxes route handlers with POST actions for all operations - Add boxes.html template with 3-panel layout matching warehouse locations module - Implement barcode generation and printing with JsBarcode and QZ Tray integration - Add browser print fallback for when QZ Tray is not available - Simplify create box form to single button with auto-generation - Fix JavaScript null reference errors with proper element validation - Convert tuple data to dictionaries for Jinja2 template compatibility - Register boxes blueprint in Flask app initialization
255 lines
7.6 KiB
Python
255 lines
7.6 KiB
Python
"""
|
|
Boxes Management Module
|
|
Handles CRUD operations for warehouse boxes
|
|
Uses boxes_crates table matching the old app structure
|
|
"""
|
|
|
|
from app.database import get_db
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def ensure_boxes_table():
|
|
"""Create boxes_crates table if it doesn't exist"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("SHOW TABLES LIKE 'boxes_crates'")
|
|
result = cursor.fetchone()
|
|
|
|
if not result:
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS boxes_crates (
|
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
box_number VARCHAR(8) NOT NULL UNIQUE,
|
|
status ENUM('open', 'closed') DEFAULT 'open',
|
|
location_id BIGINT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
created_by VARCHAR(100),
|
|
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL,
|
|
INDEX idx_box_number (box_number),
|
|
INDEX idx_status (status)
|
|
)
|
|
''')
|
|
conn.commit()
|
|
|
|
cursor.close()
|
|
logger.info("boxes_crates table ensured")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error ensuring boxes_crates table: {e}")
|
|
return False
|
|
|
|
|
|
def generate_box_number():
|
|
"""Generate next box number with 8 digits (00000001, 00000002, etc.)"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT MAX(CAST(box_number AS UNSIGNED)) FROM boxes_crates")
|
|
result = cursor.fetchone()
|
|
cursor.close()
|
|
|
|
if result and result[0]:
|
|
next_number = int(result[0]) + 1
|
|
else:
|
|
next_number = 1
|
|
|
|
return str(next_number).zfill(8)
|
|
except Exception as e:
|
|
logger.error(f"Error generating box number: {e}")
|
|
return "00000001"
|
|
|
|
|
|
def add_box(created_by=None):
|
|
"""Add a new box/crate with auto-generated number"""
|
|
try:
|
|
ensure_boxes_table()
|
|
|
|
box_number = generate_box_number()
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute(
|
|
"INSERT INTO boxes_crates (box_number, status, created_by) VALUES (%s, %s, %s)",
|
|
(box_number, 'open', created_by)
|
|
)
|
|
conn.commit()
|
|
cursor.close()
|
|
|
|
logger.info(f"Box {box_number} created successfully")
|
|
return True, f"Box {box_number} created successfully"
|
|
except Exception as e:
|
|
logger.error(f"Error adding box: {e}")
|
|
return False, f"Error creating box: {str(e)}"
|
|
|
|
|
|
def get_all_boxes():
|
|
"""Get all boxes with their location information"""
|
|
try:
|
|
ensure_boxes_table()
|
|
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT
|
|
b.id,
|
|
b.box_number,
|
|
b.status,
|
|
COALESCE(l.location_code, 'Not assigned') as location_code,
|
|
b.created_at,
|
|
b.updated_at,
|
|
b.created_by,
|
|
b.location_id
|
|
FROM boxes_crates b
|
|
LEFT JOIN warehouse_locations l ON b.location_id = l.id
|
|
ORDER BY b.created_at DESC
|
|
''')
|
|
|
|
boxes = cursor.fetchall()
|
|
cursor.close()
|
|
|
|
return boxes if boxes else []
|
|
except Exception as e:
|
|
logger.error(f"Error getting all boxes: {e}")
|
|
return []
|
|
|
|
|
|
def get_box_by_id(box_id):
|
|
"""Get a single box by ID"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT
|
|
b.id,
|
|
b.box_number,
|
|
b.status,
|
|
b.location_id,
|
|
b.created_by,
|
|
b.created_at,
|
|
b.updated_at
|
|
FROM boxes_crates b
|
|
WHERE b.id = %s
|
|
''', (box_id,))
|
|
|
|
box = cursor.fetchone()
|
|
cursor.close()
|
|
|
|
return box
|
|
except Exception as e:
|
|
logger.error(f"Error getting box by ID: {e}")
|
|
return None
|
|
|
|
|
|
def update_box(box_id, status=None, location_id=None):
|
|
"""Update box status or location"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
if status and location_id is not None:
|
|
cursor.execute(
|
|
"UPDATE boxes_crates SET status = %s, location_id = %s WHERE id = %s",
|
|
(status, location_id if location_id else None, box_id)
|
|
)
|
|
elif status:
|
|
cursor.execute(
|
|
"UPDATE boxes_crates SET status = %s WHERE id = %s",
|
|
(status, box_id)
|
|
)
|
|
elif location_id is not None:
|
|
cursor.execute(
|
|
"UPDATE boxes_crates SET location_id = %s WHERE id = %s",
|
|
(location_id if location_id else None, box_id)
|
|
)
|
|
|
|
conn.commit()
|
|
cursor.close()
|
|
|
|
logger.info(f"Box {box_id} updated successfully")
|
|
return True, "Box updated successfully"
|
|
except Exception as e:
|
|
logger.error(f"Error updating box: {e}")
|
|
return False, f"Error updating box: {str(e)}"
|
|
|
|
|
|
def delete_box(box_id):
|
|
"""Delete a single box"""
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("DELETE FROM boxes_crates WHERE id = %s", (box_id,))
|
|
conn.commit()
|
|
cursor.close()
|
|
|
|
logger.info(f"Box {box_id} deleted successfully")
|
|
return True, "Box deleted successfully"
|
|
except Exception as e:
|
|
logger.error(f"Error deleting box: {e}")
|
|
return False, f"Error deleting box: {str(e)}"
|
|
|
|
|
|
def delete_multiple_boxes(box_ids_str):
|
|
"""Delete multiple boxes"""
|
|
try:
|
|
if not box_ids_str:
|
|
return False, "No boxes selected"
|
|
|
|
# Parse box IDs
|
|
box_ids = [int(x) for x in box_ids_str.split(',') if x.strip()]
|
|
|
|
if not box_ids:
|
|
return False, "No valid box IDs provided"
|
|
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
placeholders = ','.join(['%s'] * len(box_ids))
|
|
cursor.execute(f"DELETE FROM boxes_crates WHERE id IN ({placeholders})", box_ids)
|
|
conn.commit()
|
|
cursor.close()
|
|
|
|
logger.info(f"Deleted {len(box_ids)} boxes")
|
|
return True, f"Deleted {len(box_ids)} box(es) successfully"
|
|
except Exception as e:
|
|
logger.error(f"Error deleting multiple boxes: {e}")
|
|
return False, f"Error deleting boxes: {str(e)}"
|
|
|
|
|
|
def get_box_stats():
|
|
"""Get box statistics"""
|
|
try:
|
|
ensure_boxes_table()
|
|
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM boxes_crates")
|
|
total = cursor.fetchone()[0] if cursor.fetchone() else 0
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM boxes_crates WHERE status = 'open'")
|
|
result = cursor.fetchone()
|
|
open_count = result[0] if result else 0
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM boxes_crates WHERE status = 'closed'")
|
|
result = cursor.fetchone()
|
|
closed_count = result[0] if result else 0
|
|
|
|
cursor.close()
|
|
|
|
return {
|
|
'total': total,
|
|
'open': open_count,
|
|
'closed': closed_count
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Error getting box statistics: {e}")
|
|
return {'total': 0, 'open': 0, 'closed': 0}
|