Files
quality_app-v2/app/templates/modules/warehouse/inventory.html
Quality App Developer 07f77603eb 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
2026-01-30 12:30:56 +02:00

553 lines
19 KiB
HTML

{% extends "base.html" %}
{% block title %}Warehouse Inventory - CP Articles{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-12">
<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-md-6">
<div class="card shadow-sm">
<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="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>
<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>
<!-- 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="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 %}