Implement boxes management module with auto-numbered box creation

- 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
This commit is contained in:
Quality App Developer
2026-01-26 22:08:31 +02:00
parent 3c5a273a89
commit e1f3302c6b
37 changed files with 8429 additions and 66 deletions

View File

@@ -0,0 +1,254 @@
"""
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}