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,3 @@
"""
Warehouse Module - Initialization
"""

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}

View File

@@ -0,0 +1,101 @@
"""
Boxes Management Routes
"""
from flask import Blueprint, render_template, session, redirect, url_for, request, flash
from app.modules.warehouse.boxes import (
get_all_boxes, add_box, update_box, delete_box, delete_multiple_boxes,
get_box_by_id, get_box_stats, ensure_boxes_table
)
from app.modules.warehouse.warehouse import get_all_locations
import logging
logger = logging.getLogger(__name__)
boxes_bp = Blueprint('boxes', __name__, url_prefix='/warehouse/boxes')
@boxes_bp.route('/', methods=['GET', 'POST'])
def manage_boxes():
"""Manage warehouse boxes page"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
# Ensure table exists
ensure_boxes_table()
message = None
message_type = 'info'
if request.method == 'POST':
action = request.form.get('action', '')
user_id = session.get('user_id', 'System')
# Add new box (auto-numbered)
if action == 'add_box':
success, msg = add_box(created_by=user_id)
message = msg
message_type = 'success' if success else 'danger'
# Update box status or location
elif action == 'edit_box':
box_id = request.form.get('box_id', '')
status = request.form.get('status', '')
location_id = request.form.get('location_id', '')
location_id = int(location_id) if location_id and location_id.isdigit() else None
success, msg = update_box(box_id, status=status if status else None, location_id=location_id)
message = msg
message_type = 'success' if success else 'danger'
# Change status
elif action == 'toggle_status':
box_id = request.form.get('box_id', '')
current_status = request.form.get('current_status', 'open')
new_status = 'closed' if current_status == 'open' else 'open'
success, msg = update_box(box_id, status=new_status)
message = msg
message_type = 'success' if success else 'danger'
# Delete single box
elif action == 'delete_box':
box_id = request.form.get('box_id', '')
success, msg = delete_box(box_id)
message = msg
message_type = 'success' if success else 'danger'
# Delete multiple boxes
elif action == 'delete_multiple':
box_ids = request.form.get('delete_ids', '')
success, msg = delete_multiple_boxes(box_ids)
message = msg
message_type = 'success' if success else 'danger'
# Get data and convert tuples to dictionaries
boxes_data = get_all_boxes()
boxes = []
for box_tuple in boxes_data:
boxes.append({
'id': box_tuple[0],
'box_number': box_tuple[1],
'status': box_tuple[2],
'location_code': box_tuple[3],
'created_at': box_tuple[4],
'updated_at': box_tuple[5],
'created_by': box_tuple[6],
'location_id': box_tuple[7]
})
locations = get_all_locations()
stats = get_box_stats()
return render_template(
'modules/warehouse/boxes.html',
boxes=boxes,
locations=locations,
stats=stats,
message=message,
message_type=message_type
)

View File

@@ -0,0 +1,125 @@
"""
Warehouse Module Routes
"""
from flask import Blueprint, render_template, session, redirect, url_for, request, flash, jsonify
from app.modules.warehouse.warehouse import (
get_all_locations, add_location, update_location, delete_location,
delete_multiple_locations, get_location_by_id
)
import logging
logger = logging.getLogger(__name__)
warehouse_bp = Blueprint('warehouse', __name__, url_prefix='/warehouse')
@warehouse_bp.route('/', methods=['GET'])
def warehouse_index():
"""Warehouse module main page - launcher for all warehouse operations"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/index.html')
@warehouse_bp.route('/set-boxes-locations', methods=['GET', 'POST'])
def set_boxes_locations():
"""Set boxes locations - add or update articles in warehouse inventory"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/set_boxes_locations.html')
@warehouse_bp.route('/locations', methods=['GET', 'POST'])
def locations():
"""Create and manage warehouse locations"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
message = None
message_type = 'info'
if request.method == 'POST':
# Handle edit location
if request.form.get('edit_location'):
location_id = request.form.get('location_id', '')
size = request.form.get('edit_size', '').strip()
description = request.form.get('edit_description', '').strip()
try:
location_id = int(location_id)
success, msg = update_location(location_id, None, size if size else None, description if description else None)
message = msg
message_type = 'success' if success else 'error'
except Exception as e:
message = f"Error: {str(e)}"
message_type = 'error'
# Handle delete locations
elif request.form.get('delete_locations'):
delete_ids_str = request.form.get('delete_ids', '')
try:
location_ids = [int(id.strip()) for id in delete_ids_str.split(',') if id.strip().isdigit()]
success, msg = delete_multiple_locations(location_ids)
message = msg
message_type = 'success' if success else 'error'
except Exception as e:
message = f"Error: {str(e)}"
message_type = 'error'
# Handle add location
elif request.form.get('add_location'):
location_code = request.form.get('location_code', '').strip()
size = request.form.get('size', '').strip()
description = request.form.get('description', '').strip()
if not location_code:
message = "Location code is required"
message_type = 'error'
else:
success, msg = add_location(location_code, size if size else None, description if description else None)
message = msg
message_type = 'success' if success else 'error'
# Get all locations
locations_list = get_all_locations()
return render_template('modules/warehouse/locations.html',
locations=locations_list,
message=message,
message_type=message_type)
@warehouse_bp.route('/boxes', methods=['GET', 'POST'])
def boxes():
"""Manage boxes and crates in the warehouse"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/boxes.html')
@warehouse_bp.route('/inventory', methods=['GET'])
def inventory():
"""View warehouse inventory - products, boxes, and locations"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/inventory.html')
@warehouse_bp.route('/reports', methods=['GET'])
def reports():
"""Warehouse activity and inventory reports"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/reports.html')
@warehouse_bp.route('/test-barcode', methods=['GET'])
def test_barcode():
"""Test barcode printing functionality"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/test_barcode.html')

View File

@@ -0,0 +1,213 @@
"""
Warehouse Module - Helper Functions
Provides functions for warehouse operations
"""
import logging
from app.database import get_db
logger = logging.getLogger(__name__)
def ensure_warehouse_locations_table():
"""Ensure warehouse_locations table exists"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS warehouse_locations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
location_code VARCHAR(12) UNIQUE NOT NULL,
size INT,
description VARCHAR(250),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_location_code (location_code)
)
""")
conn.commit()
cursor.close()
logger.info("warehouse_locations table ensured")
return True
except Exception as e:
logger.error(f"Error ensuring warehouse_locations table: {e}")
return False
def add_location(location_code, size, description):
"""Add a new warehouse location"""
try:
ensure_warehouse_locations_table()
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
INSERT INTO warehouse_locations (location_code, size, description)
VALUES (%s, %s, %s)
""", (location_code, size if size else None, description))
conn.commit()
cursor.close()
return True, "Location added successfully."
except Exception as e:
if "Duplicate entry" in str(e):
return False, f"Failed: Location code '{location_code}' already exists."
logger.error(f"Error adding location: {e}")
return False, f"Error adding location: {str(e)}"
def get_all_locations():
"""Get all warehouse locations"""
try:
ensure_warehouse_locations_table()
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
SELECT id, location_code, size, description, created_at, updated_at
FROM warehouse_locations
ORDER BY id DESC
""")
locations = cursor.fetchall()
cursor.close()
result = []
for loc in locations:
result.append({
'id': loc[0],
'location_code': loc[1],
'size': loc[2],
'description': loc[3],
'created_at': loc[4],
'updated_at': loc[5]
})
return result
except Exception as e:
logger.error(f"Error getting locations: {e}")
return []
def get_location_by_id(location_id):
"""Get a specific location by ID"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
SELECT id, location_code, size, description, created_at, updated_at
FROM warehouse_locations
WHERE id = %s
""", (location_id,))
loc = cursor.fetchone()
cursor.close()
if loc:
return {
'id': loc[0],
'location_code': loc[1],
'size': loc[2],
'description': loc[3],
'created_at': loc[4],
'updated_at': loc[5]
}
return None
except Exception as e:
logger.error(f"Error getting location: {e}")
return None
def update_location(location_id, location_code=None, size=None, description=None):
"""Update a warehouse location
Args:
location_id: ID of location to update
location_code: New location code (optional - cannot be changed in form)
size: New size (optional)
description: New description (optional)
"""
try:
conn = get_db()
cursor = conn.cursor()
# Build update query dynamically
updates = []
params = []
if location_code:
updates.append("location_code = %s")
params.append(location_code)
if size is not None:
updates.append("size = %s")
params.append(size)
if description is not None:
updates.append("description = %s")
params.append(description)
if not updates:
return False, "No fields to update"
params.append(location_id)
query = f"UPDATE warehouse_locations SET {', '.join(updates)} WHERE id = %s"
cursor.execute(query, params)
conn.commit()
cursor.close()
return True, "Location updated successfully."
except Exception as e:
if "Duplicate entry" in str(e):
return False, f"Failed: Location code already exists."
logger.error(f"Error updating location: {e}")
return False, f"Error updating location: {str(e)}"
def delete_location(location_id):
"""Delete a warehouse location"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM warehouse_locations WHERE id = %s", (location_id,))
conn.commit()
cursor.close()
return True, "Location deleted successfully."
except Exception as e:
logger.error(f"Error deleting location: {e}")
return False, f"Error deleting location: {str(e)}"
def delete_multiple_locations(location_ids):
"""Delete multiple warehouse locations"""
try:
if not location_ids:
return False, "No locations to delete."
conn = get_db()
cursor = conn.cursor()
deleted_count = 0
for loc_id in location_ids:
try:
cursor.execute("DELETE FROM warehouse_locations WHERE id = %s", (int(loc_id),))
if cursor.rowcount > 0:
deleted_count += 1
except:
pass
conn.commit()
cursor.close()
return True, f"Deleted {deleted_count} location(s)."
except Exception as e:
logger.error(f"Error deleting multiple locations: {e}")
return False, f"Error deleting locations: {str(e)}"