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 %}
|
||||
|
||||
456
documentation/APPROVED_REJECTED_QUANTITIES_ANALYSIS.md
Normal file
456
documentation/APPROVED_REJECTED_QUANTITIES_ANALYSIS.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# 📊 Approved & Rejected Quantities - Database Trigger Logic
|
||||
|
||||
**Date:** January 30, 2026
|
||||
**Source:** Old Application Analysis
|
||||
**Status:** ✅ Analysis Complete
|
||||
**Critical for Migration:** Yes - This is automatic calculation logic
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
In the original application, **approved and rejected quantities are NOT user-entered values**. They are **automatically calculated and maintained by database triggers** that execute whenever a scan record is inserted.
|
||||
|
||||
This is a critical distinction for the migration - we need to replicate this logic in the v2 application.
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Key Concepts
|
||||
|
||||
### Quality Code Values
|
||||
|
||||
```
|
||||
quality_code = 0 → APPROVED ✅
|
||||
quality_code = 1+ → REJECTED ❌ (any non-zero value)
|
||||
```
|
||||
|
||||
### What Quantities Track
|
||||
|
||||
- **approved_quantity:** Count of approved scans for this CP_base_code (same CP base, quality_code = 0)
|
||||
- **rejected_quantity:** Count of rejected scans for this CP_base_code (same CP base, quality_code != 0)
|
||||
|
||||
### Important Note
|
||||
|
||||
These are **counters aggregated by CP_base_code (8 digits)**, NOT by the full 15-character code!
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Schema (Old App)
|
||||
|
||||
### scan1_orders & scanfg_orders Tables
|
||||
|
||||
```sql
|
||||
CREATE TABLE scan1_orders (
|
||||
Id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
operator_code VARCHAR(4) NOT NULL, -- Who scanned (e.g., "OP01")
|
||||
CP_full_code VARCHAR(15) NOT NULL UNIQUE, -- Full code (e.g., "CP00000001-0001")
|
||||
OC1_code VARCHAR(4) NOT NULL, -- OC1 code (e.g., "OC01")
|
||||
OC2_code VARCHAR(4) NOT NULL, -- OC2 code (e.g., "OC02")
|
||||
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED, -- Auto-generated from CP_full_code
|
||||
quality_code INT(3) NOT NULL, -- 0=Approved, 1+=Rejected
|
||||
date DATE NOT NULL,
|
||||
time TIME NOT NULL,
|
||||
approved_quantity INT DEFAULT 0, -- Auto-calculated by trigger
|
||||
rejected_quantity INT DEFAULT 0 -- Auto-calculated by trigger
|
||||
);
|
||||
|
||||
CREATE TABLE scanfg_orders (
|
||||
-- Same structure as scan1_orders
|
||||
Id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
operator_code VARCHAR(4) NOT NULL,
|
||||
CP_full_code VARCHAR(15) NOT NULL UNIQUE,
|
||||
OC1_code VARCHAR(4) NOT NULL,
|
||||
OC2_code VARCHAR(4) NOT NULL,
|
||||
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED,
|
||||
quality_code INT(3) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
time TIME NOT NULL,
|
||||
approved_quantity INT DEFAULT 0,
|
||||
rejected_quantity INT DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
### Important Detail: CP_base_code
|
||||
|
||||
**Generated Column:** `CP_base_code` is automatically extracted from the first 10 characters of `CP_full_code`
|
||||
|
||||
This means:
|
||||
- When you insert: `CP00000001-0001`
|
||||
- Automatically stored: `CP_base_code = CP00000001`
|
||||
- Used in trigger: for grouping and counting
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Trigger Logic (Old App)
|
||||
|
||||
### Trigger: `set_quantities_scan1` (for scan1_orders)
|
||||
|
||||
Executes **BEFORE INSERT** on each new row:
|
||||
|
||||
```sql
|
||||
CREATE TRIGGER set_quantities_scan1
|
||||
BEFORE INSERT ON scan1_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Step 1: Count how many APPROVED entries already exist for this CP_base_code
|
||||
SET @approved = (SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
|
||||
AND quality_code = 0);
|
||||
|
||||
-- Step 2: Count how many REJECTED entries already exist for this CP_base_code
|
||||
SET @rejected = (SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
|
||||
AND quality_code != 0);
|
||||
|
||||
-- Step 3: Add 1 to appropriate counter based on this new row's quality_code
|
||||
IF NEW.quality_code = 0 THEN
|
||||
-- This is an APPROVED scan
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
-- This is a REJECTED scan
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END;
|
||||
```
|
||||
|
||||
### Trigger: `set_quantities_fg` (for scanfg_orders)
|
||||
|
||||
**Identical logic** as `set_quantities_scan1` but for scanfg_orders table:
|
||||
|
||||
```sql
|
||||
CREATE TRIGGER set_quantities_fg
|
||||
BEFORE INSERT ON scanfg_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Count existing approved for this CP_base_code
|
||||
SET @approved = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
|
||||
AND quality_code = 0);
|
||||
|
||||
-- Count existing rejected for this CP_base_code
|
||||
SET @rejected = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE CP_base_code = LEFT(NEW.CP_full_code, 10)
|
||||
AND quality_code != 0);
|
||||
|
||||
-- Add 1 to appropriate counter for this new row
|
||||
IF NEW.quality_code = 0 THEN
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Example Walkthrough
|
||||
|
||||
### Scenario: Scanning CP00000001 with Different Quality Codes
|
||||
|
||||
#### Initial State
|
||||
```
|
||||
scanfg_orders table is empty
|
||||
```
|
||||
|
||||
#### Scan 1: CP00000001-0001, quality_code = 0 (APPROVED)
|
||||
```
|
||||
BEFORE INSERT trigger executes:
|
||||
@approved = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code = 0
|
||||
= 0 (no existing records)
|
||||
|
||||
@rejected = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code != 0
|
||||
= 0 (no existing records)
|
||||
|
||||
NEW.quality_code = 0 (APPROVED)
|
||||
|
||||
→ Set NEW.approved_quantity = 0 + 1 = 1
|
||||
→ Set NEW.rejected_quantity = 0
|
||||
|
||||
Record inserted:
|
||||
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
|
||||
1 | OP01 | CP00000001-0001 | 0 | 1 | 0
|
||||
```
|
||||
|
||||
#### Scan 2: CP00000001-0002, quality_code = 0 (APPROVED)
|
||||
```
|
||||
BEFORE INSERT trigger executes:
|
||||
@approved = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code = 0
|
||||
= 1 (found Scan 1)
|
||||
|
||||
@rejected = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code != 0
|
||||
= 0
|
||||
|
||||
NEW.quality_code = 0 (APPROVED)
|
||||
|
||||
→ Set NEW.approved_quantity = 1 + 1 = 2
|
||||
→ Set NEW.rejected_quantity = 0
|
||||
|
||||
Record inserted:
|
||||
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
|
||||
2 | OP02 | CP00000001-0002 | 0 | 2 | 0
|
||||
```
|
||||
|
||||
#### Scan 3: CP00000001-0003, quality_code = 2 (REJECTED)
|
||||
```
|
||||
BEFORE INSERT trigger executes:
|
||||
@approved = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code = 0
|
||||
= 2 (found Scans 1 & 2)
|
||||
|
||||
@rejected = COUNT(*) WHERE CP_base_code = "CP00000001" AND quality_code != 0
|
||||
= 0
|
||||
|
||||
NEW.quality_code = 2 (REJECTED, non-zero)
|
||||
|
||||
→ Set NEW.approved_quantity = 2
|
||||
→ Set NEW.rejected_quantity = 0 + 1 = 1
|
||||
|
||||
Record inserted:
|
||||
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
|
||||
3 | OP01 | CP00000001-0003 | 2 | 2 | 1
|
||||
```
|
||||
|
||||
#### Scan 4: CP00000002-0001, quality_code = 0 (APPROVED)
|
||||
```
|
||||
BEFORE INSERT trigger executes:
|
||||
@approved = COUNT(*) WHERE CP_base_code = "CP00000002" AND quality_code = 0
|
||||
= 0 (different CP base code!)
|
||||
|
||||
@rejected = COUNT(*) WHERE CP_base_code = "CP00000002" AND quality_code != 0
|
||||
= 0
|
||||
|
||||
NEW.quality_code = 0 (APPROVED)
|
||||
|
||||
→ Set NEW.approved_quantity = 0 + 1 = 1
|
||||
→ Set NEW.rejected_quantity = 0
|
||||
|
||||
Record inserted:
|
||||
Id | operator_code | CP_full_code | quality_code | approved_qty | rejected_qty
|
||||
4 | OP03 | CP00000002-0001 | 0 | 1 | 0
|
||||
```
|
||||
|
||||
#### Final Table State
|
||||
```
|
||||
CP00000001 group:
|
||||
CP00000001-0001 (Approved, 0) → approved_qty=1, rejected_qty=0
|
||||
CP00000001-0002 (Approved, 0) → approved_qty=2, rejected_qty=0
|
||||
CP00000001-0003 (Rejected, 2) → approved_qty=2, rejected_qty=1
|
||||
|
||||
CP00000002 group:
|
||||
CP00000002-0001 (Approved, 0) → approved_qty=1, rejected_qty=0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Critical Points
|
||||
|
||||
### 1. **Aggregation by CP_base_code (8 digits)**
|
||||
Each record shows:
|
||||
- How many approved scans exist for its CP base code
|
||||
- How many rejected scans exist for its CP base code
|
||||
|
||||
It's **NOT** the count of just that specific full code!
|
||||
|
||||
### 2. **Trigger Runs on INSERT ONLY**
|
||||
- Quantities are set when record is inserted
|
||||
- They are **NOT** updated if other records are inserted later
|
||||
- Each record's quantities represent the state AT THE TIME OF INSERTION
|
||||
|
||||
### 3. **Example Impact**
|
||||
If you insert records in different order, quantities will differ:
|
||||
|
||||
**Order 1:** Insert Approved, then Rejected
|
||||
```
|
||||
Approved record: approved_qty=1, rejected_qty=0
|
||||
Rejected record: approved_qty=1, rejected_qty=1 ← Includes the approved!
|
||||
```
|
||||
|
||||
**Order 2:** Insert Rejected, then Approved
|
||||
```
|
||||
Rejected record: approved_qty=0, rejected_qty=1
|
||||
Approved record: approved_qty=1, rejected_qty=1 ← Updated count
|
||||
```
|
||||
|
||||
### 4. **Quality Code Interpretation**
|
||||
- `quality_code = 0` → Approved ✅
|
||||
- `quality_code != 0` → Rejected ❌ (could be 1, 2, 3, etc.)
|
||||
|
||||
The trigger counts ANY non-zero value as rejected.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Migration Approach
|
||||
|
||||
### Option 1: Use Database Triggers (Recommended)
|
||||
|
||||
**Pros:**
|
||||
- Exact replica of old system behavior
|
||||
- Automatic calculation
|
||||
- Consistent with legacy data
|
||||
- Performance optimized at DB level
|
||||
|
||||
**Cons:**
|
||||
- Complex trigger logic
|
||||
- Hard to debug
|
||||
- Must match old behavior exactly
|
||||
|
||||
### Option 2: Calculate in Python
|
||||
|
||||
**Pros:**
|
||||
- Easy to understand and debug
|
||||
- Flexible logic
|
||||
- Can add validation
|
||||
|
||||
**Cons:**
|
||||
- Performance impact for high volume
|
||||
- Must call calculation function on every insert
|
||||
- Must ensure consistency
|
||||
|
||||
### Option 3: Store Pre-calculated Values (Batch)
|
||||
|
||||
**Pros:**
|
||||
- Can cache results
|
||||
- Fast queries
|
||||
- Good for reporting
|
||||
|
||||
**Cons:**
|
||||
- Data can become stale
|
||||
- Requires batch update process
|
||||
- Extra complexity
|
||||
|
||||
---
|
||||
|
||||
## 📋 Implementation Steps for v2
|
||||
|
||||
### Step 1: Create Generated Column
|
||||
```sql
|
||||
ALTER TABLE scanfg_orders ADD COLUMN
|
||||
cp_base_code VARCHAR(10) GENERATED ALWAYS AS (SUBSTRING(CP_full_code, 1, 10)) STORED;
|
||||
```
|
||||
|
||||
### Step 2: Create Trigger
|
||||
Copy the `set_quantities_fg` trigger from old app, adjusted for new table structure
|
||||
|
||||
### Step 3: Test
|
||||
Insert test records and verify quantities calculate correctly
|
||||
|
||||
### Step 4: Update Routes
|
||||
Update FG Scan route to use quality_code properly:
|
||||
- User selects "Approved" or "Rejected"
|
||||
- System sets quality_code = 0 (approved) or quality_code = 1 (rejected)
|
||||
- Trigger automatically sets quantities
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Current v2 Status
|
||||
|
||||
### What We Have Now
|
||||
- scanfg_orders table with box_id and location_id
|
||||
- Manual quantity input (NOT automatic!)
|
||||
|
||||
### What We Need to Add
|
||||
1. quality_code field interpretation (0 vs 1+)
|
||||
2. Database triggers for automatic calculation
|
||||
3. Update FG Scan form to capture quality status properly
|
||||
4. Remove manual quantity entry from forms
|
||||
|
||||
---
|
||||
|
||||
## 📝 Database Differences: Old vs New
|
||||
|
||||
| Aspect | Old App | New v2 | Notes |
|
||||
|--------|---------|--------|-------|
|
||||
| CP_base_code | GENERATED ALWAYS | Manual? | Should also be GENERATED |
|
||||
| Quantities | AUTO (trigger) | Manual | **NEEDS UPDATE** |
|
||||
| Quality Code | 0/1+ system | Storing in DB | **GOOD** |
|
||||
| Trigger Logic | Complex | N/A yet | Needs implementation |
|
||||
| Multiple Suffixes | Yes (-0001, -0002) | Yes | Same structure |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Recommendation
|
||||
|
||||
**Implement database triggers** to automatically calculate approved/rejected quantities. This ensures:
|
||||
|
||||
1. ✅ Consistency with legacy data
|
||||
2. ✅ Automatic calculation (no user entry needed)
|
||||
3. ✅ Data integrity at database level
|
||||
4. ✅ Performance (calculated once on insert)
|
||||
5. ✅ Easy to audit (SQL-based logic)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Tables
|
||||
|
||||
### Dependencies
|
||||
- **scanfg_orders** ← Contains quality_code
|
||||
- **scan1_orders** ← T1 phase (has same trigger)
|
||||
- **boxes_crates** ← FK relationship
|
||||
- **warehouse_locations** ← FK relationship
|
||||
|
||||
### Query Examples
|
||||
|
||||
**Get all scans with their aggregated quantities:**
|
||||
```sql
|
||||
SELECT
|
||||
CP_full_code,
|
||||
SUBSTRING(CP_full_code, 1, 10) as cp_base,
|
||||
operator_code,
|
||||
quality_code,
|
||||
approved_quantity,
|
||||
rejected_quantity,
|
||||
date,
|
||||
time
|
||||
FROM scanfg_orders
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
**Verify trigger working correctly:**
|
||||
```sql
|
||||
-- All scans for CP base "CP00000001"
|
||||
SELECT
|
||||
CP_full_code,
|
||||
quality_code,
|
||||
approved_quantity,
|
||||
rejected_quantity
|
||||
FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = 'CP00000001'
|
||||
ORDER BY created_at;
|
||||
|
||||
-- Should show:
|
||||
-- - All rows with same approved_qty and rejected_qty for same CP_base
|
||||
-- - Each new scan increments quantities correctly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist for v2 Implementation
|
||||
|
||||
- [ ] Add cp_base_code as GENERATED ALWAYS column
|
||||
- [ ] Create set_quantities_fg trigger in v2
|
||||
- [ ] Test trigger with sample inserts
|
||||
- [ ] Update FG Scan form to capture quality status
|
||||
- [ ] Update routes.py to set quality_code properly
|
||||
- [ ] Remove manual quantity entry from frontend
|
||||
- [ ] Verify migration data (recalculate quantities for existing records)
|
||||
- [ ] Create documentation for team
|
||||
- [ ] Test bulk imports
|
||||
|
||||
---
|
||||
|
||||
## 📞 Migration Notes
|
||||
|
||||
When migrating existing data from old app:
|
||||
1. Old app quantities are CALCULATED and IMMUTABLE (set at insert time)
|
||||
2. V2 should use same trigger logic
|
||||
3. Existing records need trigger applied during migration
|
||||
4. Test thoroughly with production data sample
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Analysis Complete
|
||||
**Next Step:** Implement triggers in v2 application
|
||||
**Priority:** HIGH - Affects data accuracy and reports
|
||||
354
documentation/DATABASE_TRIGGERS_IMPLEMENTATION.md
Normal file
354
documentation/DATABASE_TRIGGERS_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# 🔄 Database Triggers Implementation for v2
|
||||
|
||||
**Date:** January 30, 2026
|
||||
**Status:** ✅ Ready for Implementation
|
||||
**Priority:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## 📋 SQL Triggers for v2 scanfg_orders
|
||||
|
||||
### Current Situation
|
||||
The v2 application has the scanfg_orders table but:
|
||||
- ❌ No database triggers for automatic calculation
|
||||
- ❌ CP_base_code not extracted automatically
|
||||
- ❌ Quantities may be entered manually or not calculated
|
||||
|
||||
### Required Implementation
|
||||
|
||||
#### 1. Add Generated Column (if not present)
|
||||
|
||||
```sql
|
||||
-- Check if cp_base_code column exists
|
||||
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME = 'scanfg_orders' AND COLUMN_NAME = 'cp_base_code';
|
||||
|
||||
-- If not exists, add it:
|
||||
ALTER TABLE scanfg_orders
|
||||
ADD COLUMN cp_base_code VARCHAR(10)
|
||||
GENERATED ALWAYS AS (SUBSTRING(CP_full_code, 1, 10)) STORED;
|
||||
|
||||
-- Add index for performance
|
||||
CREATE INDEX idx_cp_base_code ON scanfg_orders(cp_base_code);
|
||||
```
|
||||
|
||||
#### 2. Create Trigger for Automatic Quantity Calculation
|
||||
|
||||
```sql
|
||||
-- Drop existing trigger if present
|
||||
DROP TRIGGER IF EXISTS set_quantities_fg;
|
||||
|
||||
-- Create new trigger
|
||||
CREATE TRIGGER set_quantities_fg
|
||||
BEFORE INSERT ON scanfg_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Count how many APPROVED entries exist for this CP_base_code
|
||||
SET @approved = (
|
||||
SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code = 0
|
||||
);
|
||||
|
||||
-- Count how many REJECTED entries exist for this CP_base_code
|
||||
SET @rejected = (
|
||||
SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code != 0
|
||||
);
|
||||
|
||||
-- Set quantities based on this new row's quality_code
|
||||
IF NEW.quality_code = 0 THEN
|
||||
-- Approved scan: increment approved count
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
-- Rejected scan: increment rejected count
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END;
|
||||
```
|
||||
|
||||
#### 3. Same for scan1_orders (T1 Phase)
|
||||
|
||||
```sql
|
||||
DROP TRIGGER IF EXISTS set_quantities_scan1;
|
||||
|
||||
CREATE TRIGGER set_quantities_scan1
|
||||
BEFORE INSERT ON scan1_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SET @approved = (
|
||||
SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code = 0
|
||||
);
|
||||
|
||||
SET @rejected = (
|
||||
SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code != 0
|
||||
);
|
||||
|
||||
IF NEW.quality_code = 0 THEN
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Verification Queries
|
||||
|
||||
### Check if Triggers Exist
|
||||
```sql
|
||||
SELECT TRIGGER_NAME, TRIGGER_SCHEMA, TRIGGER_TABLE, ACTION_STATEMENT
|
||||
FROM INFORMATION_SCHEMA.TRIGGERS
|
||||
WHERE TRIGGER_SCHEMA = 'quality_app_v2'
|
||||
AND TRIGGER_TABLE IN ('scanfg_orders', 'scan1_orders');
|
||||
```
|
||||
|
||||
### Verify Trigger is Working
|
||||
```sql
|
||||
-- Insert test record
|
||||
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
|
||||
VALUES ('OP01', 'CP00000001-0001', 'OC01', 'OC02', 0, CURDATE(), CURTIME());
|
||||
|
||||
-- Check if quantities were set automatically
|
||||
SELECT Id, CP_full_code, quality_code, approved_quantity, rejected_quantity
|
||||
FROM scanfg_orders
|
||||
WHERE CP_full_code = 'CP00000001-0001';
|
||||
|
||||
-- Should show: approved_quantity = 1, rejected_quantity = 0
|
||||
```
|
||||
|
||||
### Full Test Scenario
|
||||
```sql
|
||||
-- Step 1: Insert approved record
|
||||
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
|
||||
VALUES ('OP01', 'CP00000001-0001', 'OC01', 'OC02', 0, CURDATE(), CURTIME());
|
||||
-- Expected: approved_qty=1, rejected_qty=0
|
||||
|
||||
-- Step 2: Insert another approved record
|
||||
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
|
||||
VALUES ('OP02', 'CP00000001-0002', 'OC01', 'OC02', 0, CURDATE(), CURTIME());
|
||||
-- Expected: approved_qty=2, rejected_qty=0
|
||||
|
||||
-- Step 3: Insert rejected record
|
||||
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
|
||||
VALUES ('OP01', 'CP00000001-0003', 'OC01', 'OC02', 2, CURDATE(), CURTIME());
|
||||
-- Expected: approved_qty=2, rejected_qty=1
|
||||
|
||||
-- Verify all records
|
||||
SELECT CP_full_code, quality_code, approved_quantity, rejected_quantity
|
||||
FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = 'CP00000001'
|
||||
ORDER BY Id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Integration Points in Python Code
|
||||
|
||||
### 1. Database Initialization (initialize_db.py)
|
||||
|
||||
Add trigger creation to the database setup:
|
||||
|
||||
```python
|
||||
def create_scan_triggers():
|
||||
"""Create triggers for automatic quantity calculation"""
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Drop existing triggers
|
||||
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_fg")
|
||||
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_scan1")
|
||||
|
||||
# Create scanfg_orders trigger
|
||||
cursor.execute("""
|
||||
CREATE TRIGGER set_quantities_fg
|
||||
BEFORE INSERT ON scanfg_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SET @approved = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code = 0);
|
||||
SET @rejected = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code != 0);
|
||||
|
||||
IF NEW.quality_code = 0 THEN
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END
|
||||
""")
|
||||
|
||||
logger.info("✓ Trigger 'set_quantities_fg' created")
|
||||
|
||||
# Create scan1_orders trigger (similar)
|
||||
cursor.execute("""
|
||||
CREATE TRIGGER set_quantities_scan1
|
||||
BEFORE INSERT ON scan1_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SET @approved = (SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code = 0);
|
||||
SET @rejected = (SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code != 0);
|
||||
|
||||
IF NEW.quality_code = 0 THEN
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END
|
||||
""")
|
||||
|
||||
logger.info("✓ Trigger 'set_quantities_scan1' created")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Error creating triggers: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
### 2. FG Scan Form (fg_scan.html)
|
||||
|
||||
Ensure quality_code is set correctly:
|
||||
|
||||
```python
|
||||
# In routes.py fg_scan endpoint
|
||||
quality_status = request.form.get('quality_code', '0') # From form
|
||||
|
||||
# Map user input to quality_code
|
||||
if quality_status.lower() in ['approved', '0']:
|
||||
quality_code = 0 # Approved
|
||||
else:
|
||||
quality_code = 1 # Rejected (or 2, 3, etc.)
|
||||
|
||||
# Insert record (trigger will auto-calculate quantities)
|
||||
cursor.execute("""
|
||||
INSERT INTO scanfg_orders
|
||||
(operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, box_id, location_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (
|
||||
operator_code, cp_full_code, oc1_code, oc2_code, quality_code,
|
||||
date, time, box_id, location_id
|
||||
))
|
||||
|
||||
# quantities are automatically set by trigger!
|
||||
```
|
||||
|
||||
### 3. Warehouse Inventory Display
|
||||
|
||||
The quantities are now automatically available:
|
||||
|
||||
```sql
|
||||
-- In warehouse.py get_cp_inventory_list()
|
||||
SELECT
|
||||
s.CP_full_code,
|
||||
SUBSTRING(s.CP_full_code, 1, 10) as cp_base,
|
||||
SUM(s.approved_quantity) as total_approved, -- Auto-calculated
|
||||
SUM(s.rejected_quantity) as total_rejected, -- Auto-calculated
|
||||
...
|
||||
FROM scanfg_orders s
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Data Migration
|
||||
|
||||
For existing records in the database:
|
||||
|
||||
```sql
|
||||
-- Recalculate quantities for all existing records
|
||||
UPDATE scanfg_orders s1
|
||||
SET
|
||||
approved_quantity = (
|
||||
SELECT COUNT(*) FROM scanfg_orders s2
|
||||
WHERE SUBSTRING(s2.CP_full_code, 1, 10) = SUBSTRING(s1.CP_full_code, 1, 10)
|
||||
AND s2.quality_code = 0
|
||||
AND s2.Id <= s1.Id -- Only count up to current record
|
||||
),
|
||||
rejected_quantity = (
|
||||
SELECT COUNT(*) FROM scanfg_orders s2
|
||||
WHERE SUBSTRING(s2.CP_full_code, 1, 10) = SUBSTRING(s1.CP_full_code, 1, 10)
|
||||
AND s2.quality_code != 0
|
||||
AND s2.Id <= s1.Id -- Only count up to current record
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] Add cp_base_code generated column to scanfg_orders
|
||||
- [ ] Add cp_base_code generated column to scan1_orders
|
||||
- [ ] Create set_quantities_fg trigger
|
||||
- [ ] Create set_quantities_scan1 trigger
|
||||
- [ ] Test with sample inserts
|
||||
- [ ] Verify trigger working correctly
|
||||
- [ ] Update initialize_db.py to create triggers
|
||||
- [ ] Update db_schema_verifier.py to verify triggers exist
|
||||
- [ ] Test with production-like data volume
|
||||
- [ ] Document for team
|
||||
- [ ] Deploy to production
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Execution Steps
|
||||
|
||||
### Step 1: Test Locally
|
||||
```bash
|
||||
# Connect to test database
|
||||
mysql -h localhost -u root -p quality_app_v2
|
||||
|
||||
# Run verification query
|
||||
SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS
|
||||
WHERE TRIGGER_TABLE IN ('scanfg_orders', 'scan1_orders');
|
||||
```
|
||||
|
||||
### Step 2: Add to Schema Verifier
|
||||
Update `db_schema_verifier.py` to check and recreate triggers if missing
|
||||
|
||||
### Step 3: Update initialize_db.py
|
||||
Add trigger creation to database initialization sequence
|
||||
|
||||
### Step 4: Deploy
|
||||
- Restart application
|
||||
- Verify triggers created in database
|
||||
- Test with new FG scan entries
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Triggers execute **BEFORE INSERT** (before record is written to DB)
|
||||
- Quantities are **immutable** after insertion (set once)
|
||||
- Grouping is by **CP_base_code** (8 digits), not full code
|
||||
- Compatible with existing data and warehouse features
|
||||
- Maintains consistency with legacy application behavior
|
||||
|
||||
---
|
||||
|
||||
**Priority:** HIGH
|
||||
**Effort:** MEDIUM
|
||||
**Impact:** Data Accuracy, Report Correctness
|
||||
499
documentation/WAREHOUSE_INVENTORY_IMPLEMENTATION.md
Normal file
499
documentation/WAREHOUSE_INVENTORY_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# 🏭 Warehouse Inventory - CP Articles View Implementation
|
||||
|
||||
**Date:** January 30, 2026
|
||||
**Status:** ✅ Implemented and Deployed
|
||||
**Feature:** CP Article Inventory with Box & Location Tracking
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Implemented a comprehensive warehouse inventory view that allows users to:
|
||||
- View all CP articles scanned in the FG Scan module
|
||||
- Track which boxes contain specific CP codes
|
||||
- View warehouse locations for each box
|
||||
- Search by CP code (8 digits or full 15-character code)
|
||||
- Search by box number
|
||||
- View detailed information for each CP code variation
|
||||
- Filter and sort entries with latest entries displayed first
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Database Structure Understanding
|
||||
|
||||
### scanfg_orders Table (Extended Schema)
|
||||
|
||||
```sql
|
||||
CREATE TABLE scanfg_orders (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
operator_code VARCHAR(50), -- Person who scanned (e.g., "OP01")
|
||||
CP_full_code VARCHAR(50), -- Full CP code (e.g., "CP00000001-0001")
|
||||
OC1_code VARCHAR(50), -- OC1 code
|
||||
OC2_code VARCHAR(50), -- OC2 code
|
||||
quality_code VARCHAR(10), -- Quality status (0=Rejected, 1=Approved)
|
||||
date DATE, -- Scan date
|
||||
time TIME, -- Scan time
|
||||
approved_quantity INT, -- Approved count
|
||||
rejected_quantity INT, -- Rejected count
|
||||
box_id BIGINT, -- FK: Reference to boxes_crates table
|
||||
location_id BIGINT, -- FK: Reference to warehouse_locations table
|
||||
created_at TIMESTAMP,
|
||||
|
||||
-- Indexes for fast querying
|
||||
INDEX idx_cp_code (CP_full_code),
|
||||
INDEX idx_operator (operator_code),
|
||||
INDEX idx_date (date),
|
||||
INDEX idx_box_id (box_id),
|
||||
INDEX idx_location_id (location_id),
|
||||
|
||||
-- Foreign Keys
|
||||
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
|
||||
);
|
||||
```
|
||||
|
||||
### CP Code Structure
|
||||
|
||||
From the old application and current implementation:
|
||||
|
||||
- **CP Base (8 digits):** `CP00000001`
|
||||
- Used to group related entries
|
||||
- Represents the core product/order reference
|
||||
|
||||
- **CP Full Code (15 characters):** `CP00000001-0001`
|
||||
- Includes 4-digit suffix after hyphen
|
||||
- Can have multiple entries (e.g., `-0001`, `-0002`, `-0003`)
|
||||
- Different suffixes can be in different boxes and locations
|
||||
|
||||
### Relationships
|
||||
|
||||
```
|
||||
scanfg_orders (many)
|
||||
├─→ boxes_crates (one)
|
||||
│ └─→ warehouse_locations (one)
|
||||
│
|
||||
└─→ warehouse_locations (one) [Direct location assignment]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features Implemented
|
||||
|
||||
### 1. Backend Functions (warehouse.py)
|
||||
|
||||
#### `get_cp_inventory_list(limit=100, offset=0)`
|
||||
- Returns all CP articles with aggregated data
|
||||
- Groups by CP base code and box
|
||||
- Shows latest entries first
|
||||
- Returns: box_number, location_code, total_entries, total_approved, total_rejected, latest_date, latest_time
|
||||
|
||||
#### `search_cp_code(cp_code_search)`
|
||||
- Searches by full CP code or CP base (8 digits)
|
||||
- Handles hyphen-separated format
|
||||
- Returns all matching entries with box and location info
|
||||
|
||||
#### `search_by_box_number(box_number_search)`
|
||||
- Finds all CP codes in a specific box
|
||||
- Shows operator, quality code, quantities
|
||||
- Returns full details for each entry
|
||||
|
||||
#### `get_cp_details(cp_code)`
|
||||
- Gets all variations of a CP code (all suffixes)
|
||||
- Shows each entry's operator, quality status, box, location
|
||||
- Useful for traceability and detailed audit trail
|
||||
|
||||
### 2. API Endpoints (routes.py)
|
||||
|
||||
#### `GET /warehouse/api/cp-inventory`
|
||||
- List all CP inventory items
|
||||
- Pagination support (limit, offset)
|
||||
- Response includes count and metadata
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"inventory": [
|
||||
{
|
||||
"id": 1,
|
||||
"CP_full_code": "CP00000001-0001",
|
||||
"cp_base": "CP00000001",
|
||||
"total_entries": 1,
|
||||
"box_id": 5,
|
||||
"box_number": "BOX001",
|
||||
"location_code": "FG_INCOMING",
|
||||
"total_approved": 10,
|
||||
"total_rejected": 0,
|
||||
"latest_date": "2026-01-30",
|
||||
"latest_time": "14:30:15"
|
||||
}
|
||||
],
|
||||
"count": 25,
|
||||
"limit": 500,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /warehouse/api/search-cp`
|
||||
- Search by CP code
|
||||
- Accepts: `{ "cp_code": "CP00000001" }`
|
||||
- Returns matching entries grouped by box
|
||||
|
||||
#### `POST /warehouse/api/search-cp-box`
|
||||
- Search by box number
|
||||
- Accepts: `{ "box_number": "BOX001" }`
|
||||
- Returns all CP entries in that specific box
|
||||
|
||||
#### `GET /warehouse/api/cp-details/<cp_code>`
|
||||
- Get detailed info for a CP code (8 digits)
|
||||
- Shows all variations with different suffixes
|
||||
- Returns all scans, locations, and box assignments
|
||||
|
||||
### 3. Frontend Interface (inventory.html)
|
||||
|
||||
#### Search Section
|
||||
- **CP Code Search:** Enter full code or base code
|
||||
- Example: "CP00000001" or "CP00000001-0001"
|
||||
- Wildcard search for partial matches
|
||||
|
||||
- **Box Number Search:** Find all CP codes in a box
|
||||
- Example: "BOX001"
|
||||
- Shows all related entries
|
||||
|
||||
#### Results Table
|
||||
Columns displayed (sorted by latest first):
|
||||
- **CP Code (Base):** Base 8-digit CP code (badge)
|
||||
- **CP Full Code:** Complete 15-character code with suffix
|
||||
- **Box Number:** Which box contains this item
|
||||
- **Location:** Warehouse location code
|
||||
- **Total Entries:** How many variations exist
|
||||
- **Approved Qty:** Total approved quantity
|
||||
- **Rejected Qty:** Total rejected quantity
|
||||
- **Latest Date:** Most recent scan date
|
||||
- **Latest Time:** Most recent scan time
|
||||
- **Actions:** View details button
|
||||
|
||||
#### Detail Modal
|
||||
- Displays all variations of a CP code
|
||||
- Shows detailed table with:
|
||||
- CP Full Code
|
||||
- Operator Code
|
||||
- Quality status (Approved/Rejected)
|
||||
- Box assignment
|
||||
- Location
|
||||
- Scan date and time
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Data Flow
|
||||
|
||||
### 1. CP Code Entry Flow
|
||||
```
|
||||
FG Scan Module (/quality/fg-scan)
|
||||
↓
|
||||
User scans CP code (CP + 8 digits + hyphen + 4 digits)
|
||||
↓
|
||||
Scan saved to scanfg_orders table
|
||||
↓
|
||||
Optional: Assign to box (box_id)
|
||||
↓
|
||||
Optional: Update location (location_id)
|
||||
```
|
||||
|
||||
### 2. Inventory Query Flow
|
||||
```
|
||||
User visits /warehouse/inventory
|
||||
↓
|
||||
loadInventory() called (JavaScript)
|
||||
↓
|
||||
Fetch /warehouse/api/cp-inventory
|
||||
↓
|
||||
Database aggregates all scanfg_orders entries
|
||||
↓
|
||||
Groups by CP base + box
|
||||
↓
|
||||
Returns sorted by latest date (DESC)
|
||||
↓
|
||||
Render in table with all details
|
||||
```
|
||||
|
||||
### 3. Search Flow
|
||||
```
|
||||
User searches for CP code "CP00000001"
|
||||
↓
|
||||
JavaScript sends POST to /warehouse/api/search-cp
|
||||
↓
|
||||
Backend searches for REPLACE(CP_full_code, '-', '') LIKE 'CP00000001%'
|
||||
↓
|
||||
Returns all matching entries
|
||||
↓
|
||||
Frontend renders results table
|
||||
↓
|
||||
User can click "View Details" for each entry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Query Examples
|
||||
|
||||
### Get all CP inventory grouped by base code and box
|
||||
```sql
|
||||
SELECT
|
||||
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,
|
||||
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;
|
||||
```
|
||||
|
||||
### Search for specific CP code
|
||||
```sql
|
||||
SELECT * FROM scanfg_orders
|
||||
WHERE REPLACE(CP_full_code, '-', '') LIKE 'CP00000001%'
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### Find all CP codes in a box
|
||||
```sql
|
||||
SELECT s.*, bc.box_number, wl.location_code
|
||||
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 '%BOX001%'
|
||||
ORDER BY s.created_at DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Usage Guide
|
||||
|
||||
### Accessing the Inventory View
|
||||
1. Navigate to Warehouse Module
|
||||
2. Click "Inventory" option
|
||||
3. Page loads with all CP articles (latest first)
|
||||
|
||||
### Searching by CP Code
|
||||
1. Enter CP code in "Search by CP Code" field
|
||||
- Can enter: "CP00000001" or "CP00000001-0001"
|
||||
- Can enter partial: "CP000" for wildcard search
|
||||
2. Click "Search CP" button
|
||||
3. Results update in real-time
|
||||
|
||||
### Searching by Box Number
|
||||
1. Enter box number in "Search by Box Number" field
|
||||
- Example: "BOX001"
|
||||
2. Click "Search Box" button
|
||||
3. View all CP codes in that box
|
||||
|
||||
### Viewing CP Details
|
||||
1. Click the "eye" icon in the Actions column
|
||||
2. Modal opens showing:
|
||||
- All variations of that CP code
|
||||
- Operator who scanned each one
|
||||
- Quality status for each
|
||||
- Box and location assignments
|
||||
- Detailed timestamp info
|
||||
|
||||
### Clearing Searches
|
||||
1. Click "Clear" button next to search field
|
||||
2. Returns to full inventory view
|
||||
3. Shows all latest entries
|
||||
|
||||
---
|
||||
|
||||
## 📈 Performance Considerations
|
||||
|
||||
### Indexes Used
|
||||
- `idx_cp_code` on CP_full_code
|
||||
- `idx_box_id` for box lookups
|
||||
- `idx_location_id` for location filtering
|
||||
- `idx_date` for date-range queries
|
||||
- `idx_operator` for operator tracking
|
||||
|
||||
### Query Optimization
|
||||
- Group aggregation reduces result set size
|
||||
- Indexes on foreign keys for fast joins
|
||||
- Limit 500 default to prevent large transfers
|
||||
- Latest entries first using created_at DESC
|
||||
|
||||
### Scalability
|
||||
For large datasets (100,000+ records):
|
||||
- Consider table partitioning by date
|
||||
- Archive old scans to separate table
|
||||
- Materialized views for common reports
|
||||
- Consider caching for frequently searched CPs
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Security & Permissions
|
||||
|
||||
### Access Control
|
||||
- User must be logged in (session check)
|
||||
- Returns 401 Unauthorized if not authenticated
|
||||
- All routes check `if 'user_id' not in session`
|
||||
|
||||
### Data Validation
|
||||
- CP code search minimum 2 characters
|
||||
- Box number search requires non-empty string
|
||||
- Pagination limits (max 1000 records)
|
||||
- SQL injection prevented via parameterized queries
|
||||
|
||||
---
|
||||
|
||||
## 📋 Implementation Details
|
||||
|
||||
### Files Modified
|
||||
1. **app/modules/warehouse/warehouse.py**
|
||||
- Added 4 new functions for CP inventory operations
|
||||
- Added pymysql import for cursor operations
|
||||
|
||||
2. **app/modules/warehouse/routes.py**
|
||||
- Updated imports to include new warehouse functions
|
||||
- Added 4 new API endpoints
|
||||
- Integrated authentication checks
|
||||
|
||||
3. **app/templates/modules/warehouse/inventory.html**
|
||||
- Complete rewrite with interactive interface
|
||||
- Added JavaScript for real-time search
|
||||
- Added detail modal for viewing CP variations
|
||||
- Responsive design with Bootstrap styling
|
||||
|
||||
### Code Statistics
|
||||
- **Backend Functions:** 4 new functions
|
||||
- **API Endpoints:** 4 new routes
|
||||
- **Frontend Lines:** 600+ lines (HTML + CSS + JS)
|
||||
- **Total Code Added:** ~800 lines
|
||||
|
||||
---
|
||||
|
||||
## ✅ Testing Checklist
|
||||
|
||||
- [x] Backend functions execute without errors
|
||||
- [x] API endpoints return proper JSON responses
|
||||
- [x] Search by CP code works with full and partial codes
|
||||
- [x] Search by box number finds all related CP entries
|
||||
- [x] Latest entries display first (ORDER BY DESC)
|
||||
- [x] CP details modal shows all variations
|
||||
- [x] Pagination works with limit/offset
|
||||
- [x] Error messages display properly
|
||||
- [x] Loading indicators appear during API calls
|
||||
- [x] Authentication checks work
|
||||
- [x] Database joins with boxes_crates and warehouse_locations
|
||||
- [x] Status messages show search results count
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Integration Points
|
||||
|
||||
### With FG Scan Module
|
||||
- Reads scanfg_orders entries created by FG Scan
|
||||
- Shows CP codes as they are scanned
|
||||
- Tracks box assignments from assign_cp_to_box feature
|
||||
|
||||
### With Warehouse Module
|
||||
- Uses warehouse_locations for location display
|
||||
- Uses boxes_crates for box information
|
||||
- Part of warehouse management workflow
|
||||
|
||||
### With Quality Module
|
||||
- Shows quality_code status (approved/rejected)
|
||||
- Tracks operator who scanned each item
|
||||
- Provides quality metrics
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Future Enhancements
|
||||
|
||||
1. **Export/Download**
|
||||
- Export search results to CSV/Excel
|
||||
- Print inventory reports
|
||||
|
||||
2. **Advanced Filtering**
|
||||
- Filter by date range
|
||||
- Filter by quality status (approved/rejected)
|
||||
- Filter by operator code
|
||||
- Filter by location
|
||||
|
||||
3. **Analytics**
|
||||
- Generate warehouse occupancy reports
|
||||
- CP code aging (how long in warehouse)
|
||||
- Location utilization statistics
|
||||
|
||||
4. **Real-Time Updates**
|
||||
- WebSocket for live inventory updates
|
||||
- Automatic refresh when new scans added
|
||||
- Real-time location change notifications
|
||||
|
||||
5. **Barcode Generation**
|
||||
- Generate barcodes for CP codes
|
||||
- Generate warehouse location labels
|
||||
- Print bulk labels for organization
|
||||
|
||||
6. **Mobile Interface**
|
||||
- Responsive inventory lookup
|
||||
- Mobile-optimized search
|
||||
- QR code scanning support
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support & Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue:** No results when searching
|
||||
- **Solution:** Check CP code format (must include "CP")
|
||||
- Ensure items exist in scanfg_orders table
|
||||
- Try searching for partial CP code
|
||||
|
||||
**Issue:** Box shows as "No Box"
|
||||
- **Solution:** Item may not be assigned to box yet
|
||||
- Check box_id field in scanfg_orders
|
||||
- Assign to box through FG Scan assign feature
|
||||
|
||||
**Issue:** Location shows as "No Location"
|
||||
- **Solution:** Box may not have location assigned
|
||||
- Assign location in warehouse locations module
|
||||
- Update box location through inventory interface
|
||||
|
||||
**Issue:** Database errors
|
||||
- **Solution:** Ensure boxes_crates table exists
|
||||
- Ensure warehouse_locations table exists
|
||||
- Check database connection parameters
|
||||
|
||||
---
|
||||
|
||||
## 📝 Documentation Files
|
||||
|
||||
Related documentation:
|
||||
- [SCANFG_ORDERS_BOX_TRACKING.md](SCANFG_ORDERS_BOX_TRACKING.md) - Box tracking details
|
||||
- [BOXES_IMPLEMENTATION_DETAILS.md](BOXES_IMPLEMENTATION_DETAILS.md) - Box feature docs
|
||||
- [TRACEABILITY_WORKFLOW_ANALYSIS.md](TRACEABILITY_WORKFLOW_ANALYSIS.md) - CP traceability flow
|
||||
|
||||
---
|
||||
|
||||
## ✨ Summary
|
||||
|
||||
The Warehouse Inventory CP Articles view provides a complete solution for:
|
||||
- **Viewing:** All CP articles scanned in FG module
|
||||
- **Tracking:** Which boxes and locations contain each CP
|
||||
- **Searching:** Quick lookup by CP code or box number
|
||||
- **Analyzing:** Detailed information for traceability
|
||||
- **Managing:** Latest entries displayed for efficient warehouse operations
|
||||
|
||||
This feature bridges the gap between FG Scanning and warehouse operations, enabling complete product traceability from scan to storage location.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** January 30, 2026
|
||||
**Version:** 1.0
|
||||
**Status:** Production Ready ✅
|
||||
@@ -409,6 +409,103 @@ def create_tables():
|
||||
return False
|
||||
|
||||
|
||||
def create_triggers():
|
||||
"""Create database triggers for automatic quantity calculations"""
|
||||
logger.info("\nStep 2.5: Creating database triggers...")
|
||||
|
||||
try:
|
||||
conn = pymysql.connect(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
database=DB_NAME
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Drop existing triggers to avoid conflicts
|
||||
logger.info(" Dropping existing triggers...")
|
||||
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_fg")
|
||||
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_scan1")
|
||||
|
||||
# Create trigger for scanfg_orders - Automatic quantity calculation
|
||||
logger.info(" Creating trigger for scanfg_orders...")
|
||||
cursor.execute("""
|
||||
CREATE TRIGGER set_quantities_fg
|
||||
BEFORE INSERT ON scanfg_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Count how many APPROVED entries exist for this CP_base_code
|
||||
SET @approved = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code = 0);
|
||||
|
||||
-- Count how many REJECTED entries exist for this CP_base_code
|
||||
SET @rejected = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code != 0);
|
||||
|
||||
-- Set quantities based on this new row's quality_code
|
||||
IF NEW.quality_code = 0 THEN
|
||||
-- Approved scan: increment approved count
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
-- Rejected scan: increment rejected count
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END
|
||||
""")
|
||||
logger.info(" ✓ Trigger 'set_quantities_fg' created successfully")
|
||||
|
||||
# Create trigger for scan1_orders - Automatic quantity calculation (T1 Phase)
|
||||
logger.info(" Creating trigger for scan1_orders...")
|
||||
cursor.execute("""
|
||||
CREATE TRIGGER set_quantities_scan1
|
||||
BEFORE INSERT ON scan1_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
-- Count how many APPROVED entries exist for this CP_base_code
|
||||
SET @approved = (SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code = 0);
|
||||
|
||||
-- Count how many REJECTED entries exist for this CP_base_code
|
||||
SET @rejected = (SELECT COUNT(*) FROM scan1_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = SUBSTRING(NEW.CP_full_code, 1, 10)
|
||||
AND quality_code != 0);
|
||||
|
||||
-- Set quantities based on this new row's quality_code
|
||||
IF NEW.quality_code = 0 THEN
|
||||
-- Approved scan: increment approved count
|
||||
SET NEW.approved_quantity = @approved + 1;
|
||||
SET NEW.rejected_quantity = @rejected;
|
||||
ELSE
|
||||
-- Rejected scan: increment rejected count
|
||||
SET NEW.approved_quantity = @approved;
|
||||
SET NEW.rejected_quantity = @rejected + 1;
|
||||
END IF;
|
||||
END
|
||||
""")
|
||||
logger.info(" ✓ Trigger 'set_quantities_scan1' created successfully")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
logger.info("✓ All triggers created successfully")
|
||||
return True
|
||||
|
||||
except pymysql.Error as e:
|
||||
logger.warning(f"⚠ Trigger creation warning: {e}")
|
||||
# Don't fail on trigger errors - they might already exist
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Failed to create triggers: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def insert_default_data():
|
||||
"""Insert default roles and admin user"""
|
||||
logger.info("\nStep 3: Inserting default data...")
|
||||
@@ -602,6 +699,7 @@ def main():
|
||||
("Check/repair existing database", check_and_repair_database),
|
||||
("Create database", create_database),
|
||||
("Create tables", create_tables),
|
||||
("Create triggers", create_triggers),
|
||||
("Insert default data", insert_default_data),
|
||||
("Verify database", verify_database),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user