Implement approved/rejected quantity triggers and warehouse inventory
Database Triggers Implementation: - Added automatic quantity calculation triggers for scanfg_orders - Added automatic quantity calculation triggers for scan1_orders (T1 phase) - Triggers calculate based on CP_base_code grouping (8 digits) - Quality code: 0 = approved, != 0 = rejected - Quantities set at insertion time (BEFORE INSERT trigger) - Added create_triggers() function to initialize_db.py Warehouse Inventory Enhancement: - Analyzed old app database quantity calculation logic - Created comprehensive trigger implementation guide - Added trigger verification and testing procedures - Documented data migration strategy Documentation Added: - APPROVED_REJECTED_QUANTITIES_ANALYSIS.md - Old app logic analysis - DATABASE_TRIGGERS_IMPLEMENTATION.md - v2 implementation guide - WAREHOUSE_INVENTORY_IMPLEMENTATION.md - Inventory view feature Files Modified: - initialize_db.py: Added create_triggers() function and call in main() - Documentation: 3 comprehensive guides for database and inventory management Quality Metrics: - Triggers maintain legacy compatibility - Automatic calculation ensures data consistency - Performance optimized at database level - Comprehensive testing documented
This commit is contained in:
@@ -6,7 +6,8 @@ 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
|
||||
assign_box_to_location, move_box_to_new_location,
|
||||
get_cp_inventory_list, search_cp_code, search_by_box_number, get_cp_details
|
||||
)
|
||||
import logging
|
||||
|
||||
@@ -215,3 +216,120 @@ def api_get_locations():
|
||||
|
||||
locations = get_all_locations()
|
||||
return jsonify({'success': True, 'locations': locations}), 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
|
||||
|
||||
@@ -3,6 +3,7 @@ Warehouse Module - Helper Functions
|
||||
Provides functions for warehouse operations
|
||||
"""
|
||||
import logging
|
||||
import pymysql
|
||||
from app.database import get_db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -435,3 +436,196 @@ def move_box_to_new_location(box_id, new_location_code):
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving box: {e}")
|
||||
return False, f'Error: {str(e)}', 500
|
||||
|
||||
|
||||
def get_cp_inventory_list(limit=100, offset=0):
|
||||
"""
|
||||
Get CP articles from scanfg_orders with box and location info
|
||||
Groups by CP_full_code (8 digits) to show all entries with that base CP
|
||||
Returns latest entries first
|
||||
|
||||
Returns:
|
||||
List of CP inventory records with box and location info
|
||||
"""
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
# Get all CP codes with their box and location info (latest entries first)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
s.id,
|
||||
s.CP_full_code,
|
||||
SUBSTRING(s.CP_full_code, 1, 10) as cp_base,
|
||||
COUNT(*) as total_entries,
|
||||
s.box_id,
|
||||
bc.box_number,
|
||||
wl.location_code,
|
||||
wl.id as location_id,
|
||||
MAX(s.date) as latest_date,
|
||||
MAX(s.time) as latest_time,
|
||||
SUM(s.approved_quantity) as total_approved,
|
||||
SUM(s.rejected_quantity) as total_rejected
|
||||
FROM scanfg_orders s
|
||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
||||
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
|
||||
ORDER BY MAX(s.created_at) DESC
|
||||
LIMIT %s OFFSET %s
|
||||
""", (limit, offset))
|
||||
|
||||
results = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
return results if results else []
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting CP inventory: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def search_cp_code(cp_code_search):
|
||||
"""
|
||||
Search for CP codes - can search by full CP code or CP base (8 digits)
|
||||
|
||||
Args:
|
||||
cp_code_search: Search string (can be "CP00000001" or "CP00000001-0001")
|
||||
|
||||
Returns:
|
||||
List of matching CP inventory records
|
||||
"""
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
# Remove hyphen and get base CP code if full CP provided
|
||||
search_term = cp_code_search.replace('-', '').strip().upper()
|
||||
|
||||
# Search for matching CP codes
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
s.id,
|
||||
s.CP_full_code,
|
||||
SUBSTRING(s.CP_full_code, 1, 10) as cp_base,
|
||||
COUNT(*) as total_entries,
|
||||
s.box_id,
|
||||
bc.box_number,
|
||||
wl.location_code,
|
||||
wl.id as location_id,
|
||||
MAX(s.date) as latest_date,
|
||||
MAX(s.time) as latest_time,
|
||||
SUM(s.approved_quantity) as total_approved,
|
||||
SUM(s.rejected_quantity) as total_rejected
|
||||
FROM scanfg_orders s
|
||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
||||
WHERE REPLACE(s.CP_full_code, '-', '') LIKE %s
|
||||
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
|
||||
ORDER BY MAX(s.created_at) DESC
|
||||
""", (f"{search_term}%",))
|
||||
|
||||
results = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
return results if results else []
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching CP code: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def search_by_box_number(box_number_search):
|
||||
"""
|
||||
Search for box number and get all CP codes in that box
|
||||
|
||||
Args:
|
||||
box_number_search: Box number to search for
|
||||
|
||||
Returns:
|
||||
List of CP entries in the box
|
||||
"""
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
box_search = box_number_search.strip().upper()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
s.id,
|
||||
s.CP_full_code,
|
||||
s.operator_code,
|
||||
s.quality_code,
|
||||
s.date,
|
||||
s.time,
|
||||
s.approved_quantity,
|
||||
s.rejected_quantity,
|
||||
bc.box_number,
|
||||
bc.id as box_id,
|
||||
wl.location_code,
|
||||
wl.id as location_id,
|
||||
s.created_at
|
||||
FROM scanfg_orders s
|
||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
||||
WHERE bc.box_number LIKE %s
|
||||
ORDER BY s.created_at DESC
|
||||
LIMIT 500
|
||||
""", (f"%{box_search}%",))
|
||||
|
||||
results = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
return results if results else []
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching by box number: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_cp_details(cp_code):
|
||||
"""
|
||||
Get detailed information for a specific CP code (8 digits)
|
||||
Shows all variations (with different 4-digit suffixes) and their locations
|
||||
|
||||
Args:
|
||||
cp_code: CP base code (8 digits, e.g., "CP00000001")
|
||||
|
||||
Returns:
|
||||
List of all CP variations with their box and location info
|
||||
"""
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
||||
|
||||
# Search for all entries with this CP base
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
s.id,
|
||||
s.CP_full_code,
|
||||
s.operator_code,
|
||||
s.OC1_code,
|
||||
s.OC2_code,
|
||||
s.quality_code,
|
||||
s.date,
|
||||
s.time,
|
||||
s.approved_quantity,
|
||||
s.rejected_quantity,
|
||||
bc.box_number,
|
||||
bc.id as box_id,
|
||||
wl.location_code,
|
||||
wl.id as location_id,
|
||||
wl.description,
|
||||
s.created_at
|
||||
FROM scanfg_orders s
|
||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
||||
WHERE SUBSTRING(s.CP_full_code, 1, 10) = %s
|
||||
ORDER BY s.created_at DESC
|
||||
LIMIT 1000
|
||||
""", (cp_code.upper(),))
|
||||
|
||||
results = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
return results if results else []
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting CP details: {e}")
|
||||
return []
|
||||
|
||||
@@ -1,67 +1,552 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Warehouse Inventory - Quality App v2{% endblock %}
|
||||
{% block title %}Warehouse Inventory - CP Articles{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="mb-2">
|
||||
<i class="fas fa-list"></i> Warehouse Inventory
|
||||
</h1>
|
||||
<p class="text-muted">Search and view products, boxes, and their warehouse locations</p>
|
||||
</div>
|
||||
<a href="{{ url_for('warehouse.warehouse_index') }}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Warehouse
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="h3 mb-3">
|
||||
<i class="fas fa-box"></i> Warehouse Inventory
|
||||
</h1>
|
||||
<p class="text-muted">View CP articles in warehouse with box numbers and locations. Latest entries displayed first.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Section -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-search"></i> Search Inventory</h5>
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-search"></i> Search by CP Code</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="searchProduct">Search by Product Code:</label>
|
||||
<input type="text" id="searchProduct" class="form-control" placeholder="Enter product code...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="searchLocation">Search by Location:</label>
|
||||
<input type="text" id="searchLocation" class="form-control" placeholder="Enter location code...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
id="cpCodeSearch"
|
||||
class="form-control"
|
||||
placeholder="Enter CP code (e.g., CP00000001 or CP00000001-0001)"
|
||||
autocomplete="off">
|
||||
<button class="btn btn-primary"
|
||||
type="button"
|
||||
id="searchCpBtn"
|
||||
onclick="searchByCpCode()">
|
||||
<i class="fas fa-search"></i> Search CP
|
||||
</button>
|
||||
<button class="btn btn-secondary"
|
||||
type="button"
|
||||
onclick="clearCpSearch()">
|
||||
<i class="fas fa-times"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn-primary">
|
||||
<i class="fas fa-search"></i> Search
|
||||
</button>
|
||||
<small class="text-muted d-block mt-2">
|
||||
Searches for any CP code starting with the entered text. Shows all related entries.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0"><i class="fas fa-boxes"></i> Search by Box Number</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="input-group">
|
||||
<input type="text"
|
||||
id="boxNumberSearch"
|
||||
class="form-control"
|
||||
placeholder="Enter box number (e.g., BOX001)"
|
||||
autocomplete="off">
|
||||
<button class="btn btn-info"
|
||||
type="button"
|
||||
id="searchBoxBtn"
|
||||
onclick="searchByBoxNumber()">
|
||||
<i class="fas fa-search"></i> Search Box
|
||||
</button>
|
||||
<button class="btn btn-secondary"
|
||||
type="button"
|
||||
onclick="clearBoxSearch()">
|
||||
<i class="fas fa-times"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted d-block mt-2">
|
||||
Find all CP codes in a specific box with location and operator info.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0"><i class="fas fa-box"></i> Inventory Results</h5>
|
||||
<!-- Status Messages -->
|
||||
<div id="statusAlert" class="alert alert-info d-none" role="alert">
|
||||
<i class="fas fa-info-circle"></i> <span id="statusMessage"></span>
|
||||
</div>
|
||||
|
||||
<!-- Loading Indicator -->
|
||||
<div id="loadingSpinner" class="spinner-border d-none" role="status" style="display: none;">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
<!-- Results Table -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0"><i class="fas fa-table"></i> CP Inventory</h5>
|
||||
<button class="btn btn-sm btn-light" onclick="reloadInventory()">
|
||||
<i class="fas fa-sync"></i> Reload
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="inventoryTable">
|
||||
<thead class="bg-light sticky-top">
|
||||
<tr>
|
||||
<th class="bg-primary text-white">CP Code (Base)</th>
|
||||
<th class="bg-primary text-white">CP Full Code</th>
|
||||
<th class="bg-success text-white">Box Number</th>
|
||||
<th class="bg-info text-white">Location</th>
|
||||
<th class="bg-warning text-dark">Total Entries</th>
|
||||
<th class="bg-secondary text-white">Approved Qty</th>
|
||||
<th class="bg-secondary text-white">Rejected Qty</th>
|
||||
<th class="bg-secondary text-white">Latest Date</th>
|
||||
<th class="bg-secondary text-white">Latest Time</th>
|
||||
<th class="bg-dark text-white">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
<tr>
|
||||
<td colspan="10" class="text-center text-muted py-5">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading inventory data...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-muted">
|
||||
<small>
|
||||
Total Records: <strong id="totalRecords">0</strong> |
|
||||
Showing: <strong id="showingRecords">0</strong> |
|
||||
Last Updated: <strong id="lastUpdated">-</strong>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CP Details Modal -->
|
||||
<div class="modal fade" id="cpDetailsModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-details"></i> CP Code Details
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">
|
||||
<i class="fas fa-info-circle"></i> Inventory search feature coming soon...
|
||||
</p>
|
||||
<div class="modal-body">
|
||||
<div id="cpDetailsContent">
|
||||
<i class="fas fa-spinner fa-spin"></i> Loading details...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sticky-top {
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.cp-code-mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentSearchType = 'all';
|
||||
let inventoryData = [];
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadInventory();
|
||||
|
||||
// Auto-search on Enter key
|
||||
document.getElementById('cpCodeSearch').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') searchByCpCode();
|
||||
});
|
||||
|
||||
document.getElementById('boxNumberSearch').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') searchByBoxNumber();
|
||||
});
|
||||
});
|
||||
|
||||
function showStatus(message, type = 'info') {
|
||||
const alert = document.getElementById('statusAlert');
|
||||
const messageEl = document.getElementById('statusMessage');
|
||||
messageEl.textContent = message;
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.classList.remove('d-none');
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
alert.classList.add('d-none');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('loadingSpinner').classList.remove('d-none');
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
document.getElementById('loadingSpinner').classList.add('d-none');
|
||||
}
|
||||
|
||||
function loadInventory() {
|
||||
showLoading();
|
||||
currentSearchType = 'all';
|
||||
document.getElementById('cpCodeSearch').value = '';
|
||||
document.getElementById('boxNumberSearch').value = '';
|
||||
|
||||
fetch('/warehouse/api/cp-inventory?limit=500&offset=0')
|
||||
.then(response => {
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
inventoryData = data.inventory;
|
||||
renderTable(data.inventory);
|
||||
document.getElementById('totalRecords').textContent = data.count;
|
||||
document.getElementById('showingRecords').textContent = data.count;
|
||||
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
|
||||
showStatus(`Loaded ${data.count} inventory items`, 'success');
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showStatus(`Error loading inventory: ${error.message}`, 'danger');
|
||||
})
|
||||
.finally(() => hideLoading());
|
||||
}
|
||||
|
||||
function reloadInventory() {
|
||||
loadInventory();
|
||||
}
|
||||
|
||||
function searchByCpCode() {
|
||||
const cpCode = document.getElementById('cpCodeSearch').value.trim();
|
||||
|
||||
if (!cpCode) {
|
||||
showStatus('Please enter a CP code', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
currentSearchType = 'cp';
|
||||
|
||||
fetch('/warehouse/api/search-cp', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ cp_code: cpCode })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
inventoryData = data.results;
|
||||
renderTable(data.results);
|
||||
document.getElementById('totalRecords').textContent = data.count;
|
||||
document.getElementById('showingRecords').textContent = data.count;
|
||||
showStatus(`Found ${data.count} entries for CP code: ${cpCode}`, 'success');
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showStatus(`Error searching CP code: ${error.message}`, 'danger');
|
||||
})
|
||||
.finally(() => hideLoading());
|
||||
}
|
||||
|
||||
function clearCpSearch() {
|
||||
document.getElementById('cpCodeSearch').value = '';
|
||||
loadInventory();
|
||||
}
|
||||
|
||||
function searchByBoxNumber() {
|
||||
const boxNumber = document.getElementById('boxNumberSearch').value.trim();
|
||||
|
||||
if (!boxNumber) {
|
||||
showStatus('Please enter a box number', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
currentSearchType = 'box';
|
||||
|
||||
fetch('/warehouse/api/search-cp-box', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ box_number: boxNumber })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
inventoryData = data.results;
|
||||
renderBoxSearchTable(data.results);
|
||||
document.getElementById('totalRecords').textContent = data.count;
|
||||
document.getElementById('showingRecords').textContent = data.count;
|
||||
showStatus(`Found ${data.count} CP entries in box: ${boxNumber}`, 'success');
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showStatus(`Error searching by box number: ${error.message}`, 'danger');
|
||||
})
|
||||
.finally(() => hideLoading());
|
||||
}
|
||||
|
||||
function clearBoxSearch() {
|
||||
document.getElementById('boxNumberSearch').value = '';
|
||||
loadInventory();
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No inventory records found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.map(item => `
|
||||
<tr>
|
||||
<td>
|
||||
<span class="cp-code-mono badge bg-primary">
|
||||
${item.cp_base || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="cp-code-mono">
|
||||
${item.CP_full_code || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">
|
||||
${item.box_number ? `BOX ${item.box_number}` : 'No Box'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
${item.location_code || 'No Location'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-warning text-dark">
|
||||
${item.total_entries || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">
|
||||
${item.total_approved || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-danger">
|
||||
${item.total_rejected || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small>${formatDate(item.latest_date)}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>${item.latest_time || '-'}</small>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="viewCpDetails('${item.cp_base || item.CP_full_code}')"
|
||||
title="View CP details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderBoxSearchTable(data) {
|
||||
const tbody = document.getElementById('tableBody');
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No CP entries found in this box</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = data.map(item => `
|
||||
<tr>
|
||||
<td>
|
||||
<span class="cp-code-mono badge bg-primary">
|
||||
${item.CP_full_code ? item.CP_full_code.substring(0, 10) : 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="cp-code-mono">
|
||||
${item.CP_full_code || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">
|
||||
${item.box_number ? `BOX ${item.box_number}` : 'No Box'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">
|
||||
${item.location_code || 'No Location'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-warning text-dark">
|
||||
1
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-success">
|
||||
${item.approved_quantity || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-danger">
|
||||
${item.rejected_quantity || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small>${formatDate(item.date)}</small>
|
||||
</td>
|
||||
<td>
|
||||
<small>${item.time || '-'}</small>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary"
|
||||
onclick="viewCpDetails('${item.CP_full_code ? item.CP_full_code.substring(0, 10) : ''}')"
|
||||
title="View CP details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
function viewCpDetails(cpCode) {
|
||||
const cleanCpCode = cpCode.replace('-', '').substring(0, 10);
|
||||
|
||||
fetch(`/warehouse/api/cp-details/${cleanCpCode}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const modal = document.getElementById('cpDetailsModal');
|
||||
const content = document.getElementById('cpDetailsContent');
|
||||
|
||||
let html = `
|
||||
<h6 class="mb-3">CP Code: <span class="cp-code-mono badge bg-primary">${data.cp_code}</span></h6>
|
||||
<p class="text-muted">Total Variations: <strong>${data.count}</strong></p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>CP Full Code</th>
|
||||
<th>Operator</th>
|
||||
<th>Quality</th>
|
||||
<th>Box</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
data.details.forEach(item => {
|
||||
html += `
|
||||
<tr>
|
||||
<td><span class="cp-code-mono">${item.CP_full_code}</span></td>
|
||||
<td>${item.operator_code || '-'}</td>
|
||||
<td>
|
||||
<span class="badge ${item.quality_code === '1' ? 'bg-success' : 'bg-danger'}">
|
||||
${item.quality_code === '1' ? 'Approved' : 'Rejected'}
|
||||
</span>
|
||||
</td>
|
||||
<td>${item.box_number || 'No Box'}</td>
|
||||
<td>${item.location_code || 'No Location'}</td>
|
||||
<td><small>${formatDate(item.date)}</small></td>
|
||||
<td><small>${item.time || '-'}</small></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
|
||||
content.innerHTML = html;
|
||||
const modalObj = new bootstrap.Modal(modal);
|
||||
modalObj.show();
|
||||
} else {
|
||||
showStatus(`Error: ${data.error}`, 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showStatus(`Error loading CP details: ${error.message}`, 'danger');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user