Files
quality_app-v2/app/modules/warehouse/routes.py
Quality App Developer f54e1bebc3 feat: Add Set Orders on Boxes feature with debouncing and page refresh
- Created warehouse_orders.py module with 8 order management functions
- Added /warehouse/set-orders-on-boxes route and 7 API endpoints
- Implemented 4-tab interface: assign, find, move, and view orders
- Changed assign input from dropdown to text field with BOX validation
- Fixed location join issue in warehouse.py (use boxes_crates.location_id)
- Added debouncing flag to prevent multiple rapid form submissions
- Added page refresh after successful order assignment
- Disabled assign button during processing
- Added page refresh with 2 second delay for UX feedback
- Added CP code validation in inventory page
- Improved modal styling with theme support
- Fixed set_boxes_locations page to refresh box info after assignments
2026-02-02 01:06:03 +02:00

463 lines
17 KiB
Python

"""
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,
search_box_by_number, search_location_with_boxes,
assign_box_to_location, move_box_to_new_location,
get_cp_inventory_list, search_cp_code, search_by_box_number, get_cp_details
)
from app.modules.warehouse.warehouse_orders import (
get_unassigned_orders, get_orders_by_box, search_orders_by_cp_code,
assign_order_to_box, move_order_to_box, unassign_order_from_box,
get_all_boxes_summary
)
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('/set-orders-on-boxes', methods=['GET', 'POST'])
def set_orders_on_boxes():
"""Set orders on boxes - assign or move orders between boxes"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/set_orders_on_boxes.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')
# ============================================================================
# API Routes for Set Boxes Locations Feature
# ============================================================================
@warehouse_bp.route('/api/search-box', methods=['POST'], endpoint='api_search_box')
def api_search_box():
"""Search for a box by number"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
box_number = data.get('box_number', '').strip()
if not box_number:
return jsonify({'success': False, 'error': 'Box number is required'}), 400
success, box_data, status_code = search_box_by_number(box_number)
if success:
return jsonify({'success': True, 'box': box_data}), 200
else:
return jsonify({'success': False, 'error': f'Box "{box_number}" not found'}), status_code
@warehouse_bp.route('/api/search-location', methods=['POST'], endpoint='api_search_location')
def api_search_location():
"""Search for a location and get all boxes in it"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
location_code = data.get('location_code', '').strip()
if not location_code:
return jsonify({'success': False, 'error': 'Location code is required'}), 400
success, response_data, status_code = search_location_with_boxes(location_code)
if success:
return jsonify({'success': True, **response_data}), 200
else:
return jsonify({'success': False, 'error': response_data.get('error', 'Not found')}), status_code
@warehouse_bp.route('/api/assign-box-to-location', methods=['POST'], endpoint='api_assign_box_to_location')
def api_assign_box_to_location():
"""Assign a box to a location"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
box_id = data.get('box_id')
location_code = data.get('location_code', '').strip()
if not box_id or not location_code:
return jsonify({'success': False, 'error': 'Box ID and location code are required'}), 400
success, message, status_code = assign_box_to_location(box_id, location_code)
return jsonify({'success': success, 'message': message}), status_code
@warehouse_bp.route('/api/move-box-to-location', methods=['POST'], endpoint='api_move_box_to_location')
def api_move_box_to_location():
"""Move a box to a new location"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
box_id = data.get('box_id')
new_location_code = data.get('new_location_code', '').strip()
if not box_id or not new_location_code:
return jsonify({'success': False, 'error': 'Box ID and new location code are required'}), 400
success, message, status_code = move_box_to_new_location(box_id, new_location_code)
return jsonify({'success': success, 'message': message}), status_code
@warehouse_bp.route('/api/get-locations', methods=['GET'], endpoint='api_get_locations')
def api_get_locations():
"""Get all warehouse locations for dropdown"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
locations = get_all_locations()
return jsonify({'success': True, 'locations': locations}), 200
# ============================================================================
# API Routes for Orders Management
# ============================================================================
@warehouse_bp.route('/api/unassigned-orders', methods=['GET'], endpoint='api_unassigned_orders')
def api_unassigned_orders():
"""Get all unassigned orders"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
limit = request.args.get('limit', 100, type=int)
offset = request.args.get('offset', 0, type=int)
orders = get_unassigned_orders(limit, offset)
return jsonify({'success': True, 'orders': orders, 'count': len(orders)}), 200
except Exception as e:
logger.error(f"Error getting unassigned orders: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@warehouse_bp.route('/api/orders-by-box', methods=['POST'], endpoint='api_orders_by_box')
def api_orders_by_box():
"""Get all orders assigned to a specific box"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
box_id = data.get('box_id')
if not box_id:
return jsonify({'success': False, 'error': 'Box ID is required'}), 400
success, data_resp, status_code = get_orders_by_box(box_id)
return jsonify({'success': success, **data_resp}), status_code
@warehouse_bp.route('/api/search-orders', methods=['POST'], endpoint='api_search_orders')
def api_search_orders():
"""Search for orders by CP code"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
cp_code = data.get('cp_code', '').strip()
if not cp_code:
return jsonify({'success': False, 'error': 'CP code is required'}), 400
orders = search_orders_by_cp_code(cp_code)
return jsonify({'success': True, 'orders': orders, 'count': len(orders)}), 200
@warehouse_bp.route('/api/assign-order-to-box', methods=['POST'], endpoint='api_assign_order_to_box')
def api_assign_order_to_box():
"""Assign an order to a box"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
order_id = data.get('order_id')
box_id = data.get('box_id')
if not order_id or not box_id:
return jsonify({'success': False, 'error': 'Order ID and box ID are required'}), 400
success, message, status_code = assign_order_to_box(order_id, box_id)
return jsonify({'success': success, 'message': message}), status_code
@warehouse_bp.route('/api/move-order-to-box', methods=['POST'], endpoint='api_move_order_to_box')
def api_move_order_to_box():
"""Move an order to a different box"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
order_id = data.get('order_id')
new_box_id = data.get('new_box_id')
if not order_id or not new_box_id:
return jsonify({'success': False, 'error': 'Order ID and destination box ID are required'}), 400
success, message, status_code = move_order_to_box(order_id, new_box_id)
return jsonify({'success': success, 'message': message}), status_code
@warehouse_bp.route('/api/unassign-order', methods=['POST'], endpoint='api_unassign_order')
def api_unassign_order():
"""Remove an order from its box"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
data = request.get_json()
order_id = data.get('order_id')
if not order_id:
return jsonify({'success': False, 'error': 'Order ID is required'}), 400
success, message, status_code = unassign_order_from_box(order_id)
return jsonify({'success': success, 'message': message}), status_code
@warehouse_bp.route('/api/boxes-summary', methods=['GET'], endpoint='api_boxes_summary')
def api_boxes_summary():
"""Get summary of all boxes with order counts"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
boxes = get_all_boxes_summary()
return jsonify({'success': True, 'boxes': boxes}), 200
# ============================================================================
# API Routes for CP Inventory View
# ============================================================================
@warehouse_bp.route('/api/cp-inventory', methods=['GET'], endpoint='api_cp_inventory')
def api_cp_inventory():
"""Get CP inventory list - all CP articles with box and location info"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
limit = request.args.get('limit', 100, type=int)
offset = request.args.get('offset', 0, type=int)
# Validate pagination parameters
if limit > 1000:
limit = 1000
if limit < 1:
limit = 50
if offset < 0:
offset = 0
inventory = get_cp_inventory_list(limit=limit, offset=offset)
return jsonify({
'success': True,
'inventory': inventory,
'count': len(inventory),
'limit': limit,
'offset': offset
}), 200
except Exception as e:
logger.error(f"Error getting CP inventory: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@warehouse_bp.route('/api/search-cp', methods=['POST'], endpoint='api_search_cp')
def api_search_cp():
"""Search for CP code in warehouse inventory"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
data = request.get_json()
cp_code = data.get('cp_code', '').strip()
if not cp_code or len(cp_code) < 2:
return jsonify({'success': False, 'error': 'CP code must be at least 2 characters'}), 400
results = search_cp_code(cp_code)
return jsonify({
'success': True,
'results': results,
'count': len(results),
'search_term': cp_code
}), 200
except Exception as e:
logger.error(f"Error searching CP code: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@warehouse_bp.route('/api/search-cp-box', methods=['POST'], endpoint='api_search_cp_box')
def api_search_cp_box():
"""Search for box number and get all CP codes in it"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
data = request.get_json()
box_number = data.get('box_number', '').strip()
if not box_number or len(box_number) < 1:
return jsonify({'success': False, 'error': 'Box number is required'}), 400
results = search_by_box_number(box_number)
return jsonify({
'success': True,
'results': results,
'count': len(results),
'search_term': box_number
}), 200
except Exception as e:
logger.error(f"Error searching by box number: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@warehouse_bp.route('/api/cp-details/<cp_code>', methods=['GET'], endpoint='api_cp_details')
def api_cp_details(cp_code):
"""Get detailed information for a CP code and all its variations"""
if 'user_id' not in session:
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
try:
cp_code = cp_code.strip().upper()
# Ensure CP code is properly formatted (at least "CP00000001")
if not cp_code.startswith('CP') or len(cp_code) < 10:
return jsonify({'success': False, 'error': 'Invalid CP code format'}), 400
# Extract base CP code (first 10 characters)
cp_base = cp_code[:10]
details = get_cp_details(cp_base)
return jsonify({
'success': True,
'cp_code': cp_base,
'details': details,
'count': len(details)
}), 200
except Exception as e:
logger.error(f"Error getting CP details: {e}")
return jsonify({'success': False, 'error': str(e)}), 500