updated to set boxes , and updated to fill boxes
This commit is contained in:
@@ -14,7 +14,7 @@ def create_app():
|
||||
from app.routes import bp as main_bp, warehouse_bp
|
||||
from app.daily_mirror import daily_mirror_bp
|
||||
app.register_blueprint(main_bp, url_prefix='/')
|
||||
app.register_blueprint(warehouse_bp)
|
||||
app.register_blueprint(warehouse_bp, url_prefix='/warehouse')
|
||||
app.register_blueprint(daily_mirror_bp)
|
||||
|
||||
# Add 'now' function to Jinja2 globals
|
||||
|
||||
@@ -216,6 +216,76 @@ def create_boxes_crates_table():
|
||||
print_error(f"Failed to create boxes_crates table: {e}")
|
||||
return False
|
||||
|
||||
def create_box_contents_table():
|
||||
"""Create box_contents table for tracking CP codes in boxes"""
|
||||
print_step(6, "Creating Box Contents Tracking Table")
|
||||
|
||||
try:
|
||||
conn = mariadb.connect(**DB_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
|
||||
box_contents_query = """
|
||||
CREATE TABLE IF NOT EXISTS box_contents (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
box_id BIGINT NOT NULL,
|
||||
cp_code VARCHAR(15) NOT NULL,
|
||||
scan_id BIGINT,
|
||||
scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
scanned_by VARCHAR(100),
|
||||
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||
INDEX idx_box_id (box_id),
|
||||
INDEX idx_cp_code (cp_code)
|
||||
);
|
||||
"""
|
||||
cursor.execute(box_contents_query)
|
||||
print_success("Table 'box_contents' created successfully")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"Failed to create box_contents table: {e}")
|
||||
return False
|
||||
|
||||
def create_location_contents_table():
|
||||
"""Create location_contents table for tracking boxes in locations"""
|
||||
print_step(7, "Creating Location Contents Tracking Table")
|
||||
|
||||
try:
|
||||
conn = mariadb.connect(**DB_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
|
||||
location_contents_query = """
|
||||
CREATE TABLE IF NOT EXISTS location_contents (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
location_id BIGINT NOT NULL,
|
||||
box_id BIGINT NOT NULL,
|
||||
placed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
placed_by VARCHAR(100),
|
||||
removed_at TIMESTAMP NULL,
|
||||
removed_by VARCHAR(100),
|
||||
status ENUM('active', 'removed') DEFAULT 'active',
|
||||
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||
INDEX idx_location_id (location_id),
|
||||
INDEX idx_box_id (box_id),
|
||||
INDEX idx_status (status)
|
||||
);
|
||||
"""
|
||||
cursor.execute(location_contents_query)
|
||||
print_success("Table 'location_contents' created successfully")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"Failed to create location_contents table: {e}")
|
||||
return False
|
||||
|
||||
def create_permissions_tables():
|
||||
"""Create permission management tables"""
|
||||
print_step(5, "Creating Permission Management Tables")
|
||||
@@ -738,6 +808,8 @@ def main():
|
||||
create_order_for_labels_table,
|
||||
create_warehouse_locations_table,
|
||||
create_boxes_crates_table,
|
||||
create_box_contents_table,
|
||||
create_location_contents_table,
|
||||
create_permissions_tables,
|
||||
create_users_table_mariadb,
|
||||
# create_sqlite_tables, # Disabled - using MariaDB only
|
||||
|
||||
@@ -217,6 +217,74 @@ def get_db_connection():
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def ensure_scanfg_orders_table():
|
||||
"""Ensure scanfg_orders table exists with proper structure and trigger"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if table exists
|
||||
cursor.execute("SHOW TABLES LIKE 'scanfg_orders'")
|
||||
if cursor.fetchone():
|
||||
conn.close()
|
||||
return # Table already exists
|
||||
|
||||
print("Creating scanfg_orders table...")
|
||||
|
||||
# Create table
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS scanfg_orders (
|
||||
Id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
operator_code VARCHAR(50),
|
||||
CP_base_code VARCHAR(10),
|
||||
CP_full_code VARCHAR(15),
|
||||
OC1_code VARCHAR(50),
|
||||
OC2_code VARCHAR(50),
|
||||
quality_code INT,
|
||||
date DATE,
|
||||
time TIME,
|
||||
approved_quantity INT DEFAULT 0,
|
||||
rejected_quantity INT DEFAULT 0,
|
||||
INDEX idx_cp_base (CP_base_code),
|
||||
INDEX idx_date (date),
|
||||
INDEX idx_quality (quality_code)
|
||||
)
|
||||
""")
|
||||
|
||||
# Create trigger
|
||||
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_fg")
|
||||
cursor.execute("""
|
||||
CREATE TRIGGER set_quantities_fg
|
||||
BEFORE INSERT ON scanfg_orders
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
SET @cp_base = SUBSTRING(NEW.CP_full_code, 1, 10);
|
||||
SET @approved = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = @cp_base
|
||||
AND quality_code = 0);
|
||||
SET @rejected = (SELECT COUNT(*) FROM scanfg_orders
|
||||
WHERE SUBSTRING(CP_full_code, 1, 10) = @cp_base
|
||||
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;
|
||||
|
||||
SET NEW.CP_base_code = @cp_base;
|
||||
END
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("✅ scanfg_orders table and trigger created successfully")
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"Error creating scanfg_orders table: {e}")
|
||||
|
||||
@bp.route('/dashboard')
|
||||
def dashboard():
|
||||
print("Session user:", session.get('user'), session.get('role'))
|
||||
@@ -589,6 +657,8 @@ def logout():
|
||||
@bp.route('/fg_scan', methods=['GET', 'POST'])
|
||||
@requires_quality_module
|
||||
def fg_scan():
|
||||
# Ensure scanfg_orders table exists
|
||||
ensure_scanfg_orders_table()
|
||||
|
||||
if request.method == 'POST':
|
||||
# Handle form submission
|
||||
@@ -637,6 +707,14 @@ def fg_scan():
|
||||
except mariadb.Error as e:
|
||||
print(f"Error saving finish goods scan data: {e}")
|
||||
flash(f"Error saving scan data: {e}")
|
||||
|
||||
# Check if this is an AJAX request (for scan-to-boxes feature)
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.accept_mimetypes.best == 'application/json':
|
||||
# For AJAX requests, return JSON response without redirect
|
||||
return jsonify({'success': True, 'message': 'Scan recorded successfully'})
|
||||
|
||||
# For normal form submissions, redirect to prevent form resubmission (POST-Redirect-GET pattern)
|
||||
return redirect(url_for('main.fg_scan'))
|
||||
|
||||
# Fetch the latest scan data for display from scanfg_orders
|
||||
scan_data = []
|
||||
@@ -1336,7 +1414,7 @@ def get_fg_report_data():
|
||||
cursor.execute("""
|
||||
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||
FROM scanfg_orders
|
||||
WHERE date = ?
|
||||
WHERE date = %s
|
||||
ORDER BY date DESC, time DESC
|
||||
""", (today,))
|
||||
rows = cursor.fetchall()
|
||||
@@ -1351,7 +1429,7 @@ def get_fg_report_data():
|
||||
cursor.execute("""
|
||||
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||
FROM scanfg_orders
|
||||
WHERE date >= ?
|
||||
WHERE date >= %s
|
||||
ORDER BY date DESC, time DESC
|
||||
""", (start_date,))
|
||||
rows = cursor.fetchall()
|
||||
@@ -1365,7 +1443,7 @@ def get_fg_report_data():
|
||||
cursor.execute("""
|
||||
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||
FROM scanfg_orders
|
||||
WHERE date = ? AND quality_code != 0
|
||||
WHERE date = %s AND quality_code != 0
|
||||
ORDER BY date DESC, time DESC
|
||||
""", (today,))
|
||||
rows = cursor.fetchall()
|
||||
@@ -1380,7 +1458,7 @@ def get_fg_report_data():
|
||||
cursor.execute("""
|
||||
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||
FROM scanfg_orders
|
||||
WHERE date >= ? AND quality_code != 0
|
||||
WHERE date >= %s AND quality_code != 0
|
||||
ORDER BY date DESC, time DESC
|
||||
""", (start_date,))
|
||||
rows = cursor.fetchall()
|
||||
@@ -3947,6 +4025,20 @@ def manage_boxes():
|
||||
return manage_boxes_handler()
|
||||
|
||||
|
||||
@warehouse_bp.route('/assign_cp_to_box', methods=['POST'])
|
||||
@requires_warehouse_module
|
||||
def assign_cp_to_box():
|
||||
from app.warehouse import assign_cp_to_box_handler
|
||||
return assign_cp_to_box_handler()
|
||||
|
||||
|
||||
@warehouse_bp.route('/inventory', methods=['GET'])
|
||||
@requires_warehouse_module
|
||||
def warehouse_inventory():
|
||||
from app.warehouse import view_warehouse_inventory_handler
|
||||
return view_warehouse_inventory_handler()
|
||||
|
||||
|
||||
# Daily Mirror Route Redirects for Backward Compatibility
|
||||
@bp.route('/daily_mirror_main')
|
||||
def daily_mirror_main_route():
|
||||
|
||||
@@ -15,7 +15,128 @@
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// Global variables for scan-to-boxes feature - must be accessible by all scripts
|
||||
let scanToBoxesEnabled = false;
|
||||
let currentCpCode = null;
|
||||
|
||||
// Define scan-to-boxes functions at global scope
|
||||
async function submitScanWithBoxAssignment() {
|
||||
const form = document.getElementById('fg-scan-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
console.log('=== submitScanWithBoxAssignment called ===');
|
||||
console.log('Form data entries:');
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(` ${key}: ${value}`);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(window.location.href, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response ok:', response.ok);
|
||||
|
||||
if (response.ok) {
|
||||
currentCpCode = formData.get('cp_code');
|
||||
const defectCode = formData.get('defect_code') || '000';
|
||||
|
||||
console.log('CP Code:', currentCpCode);
|
||||
console.log('Defect Code:', defectCode);
|
||||
|
||||
showNotification('✅ Scan recorded successfully!', 'success');
|
||||
|
||||
// Only show box modal if quality code is 000
|
||||
if (defectCode === '000' || defectCode === '0') {
|
||||
console.log('Should show box modal');
|
||||
showBoxModal(currentCpCode);
|
||||
} else {
|
||||
console.log('Defect code not 000, reloading page');
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
}
|
||||
|
||||
// Clear form fields (except operator code)
|
||||
document.getElementById('cp_code').value = '';
|
||||
document.getElementById('oc1_code').value = '';
|
||||
document.getElementById('oc2_code').value = '';
|
||||
document.getElementById('defect_code').value = '';
|
||||
} else {
|
||||
console.error('Response not OK');
|
||||
showNotification('❌ Scan submission failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in submitScanWithBoxAssignment:', error);
|
||||
showNotification('❌ Error: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showBoxModal(cpCode) {
|
||||
document.getElementById('modal-cp-code').textContent = cpCode;
|
||||
document.getElementById('box-assignment-modal').style.display = 'block';
|
||||
document.getElementById('scan-box-input').value = '';
|
||||
document.getElementById('scan-box-input').focus();
|
||||
}
|
||||
|
||||
function closeBoxModal() {
|
||||
document.getElementById('box-assignment-modal').style.display = 'none';
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
async function assignCpToBox(boxNumber) {
|
||||
const response = await fetch('/warehouse/assign_cp_to_box', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
box_number: boxNumber,
|
||||
cp_code: currentCpCode
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to assign CP to box');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : type === 'warning' ? '#ff9800' : '#2196F3'};
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
border-radius: 5px;
|
||||
z-index: 10001;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||||
font-weight: bold;
|
||||
`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load toggle state FIRST
|
||||
const savedState = localStorage.getItem('scan_to_boxes_enabled');
|
||||
if (savedState === 'true') {
|
||||
scanToBoxesEnabled = true;
|
||||
}
|
||||
console.log('Initial scanToBoxesEnabled:', scanToBoxesEnabled);
|
||||
|
||||
const operatorCodeInput = document.getElementById('operator_code');
|
||||
const cpCodeInput = document.getElementById('cp_code');
|
||||
const oc1CodeInput = document.getElementById('oc1_code');
|
||||
@@ -23,6 +144,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const defectCodeInput = document.getElementById('defect_code');
|
||||
const form = document.getElementById('fg-scan-form');
|
||||
|
||||
// Set up toggle checkbox
|
||||
const toggleElement = document.getElementById('scan-to-boxes-toggle');
|
||||
if (toggleElement) {
|
||||
toggleElement.checked = scanToBoxesEnabled;
|
||||
toggleElement.addEventListener('change', function() {
|
||||
scanToBoxesEnabled = this.checked;
|
||||
localStorage.setItem('scan_to_boxes_enabled', this.checked);
|
||||
console.log('Toggle changed - Scan to boxes:', scanToBoxesEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
// Restore saved operator code from localStorage (only Quality Operator Code)
|
||||
const savedOperatorCode = localStorage.getItem('fg_scan_operator_code');
|
||||
|
||||
@@ -417,13 +549,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
localStorage.setItem('fg_scan_last_cp', cpCodeInput.value);
|
||||
localStorage.setItem('fg_scan_last_defect', defectCodeInput.value);
|
||||
|
||||
// Submit the form
|
||||
form.submit();
|
||||
// Check if scan-to-boxes is enabled and defect code is 000
|
||||
if (scanToBoxesEnabled && this.value === '000') {
|
||||
console.log('Auto-submit: Scan-to-boxes enabled, calling submitScanWithBoxAssignment');
|
||||
submitScanWithBoxAssignment();
|
||||
} else {
|
||||
console.log('Auto-submit: Normal form submission');
|
||||
// Submit the form normally
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Validate form on submit
|
||||
form.addEventListener('submit', function(e) {
|
||||
form.addEventListener('submit', async function(e) {
|
||||
let hasError = false;
|
||||
|
||||
if (!operatorCodeInput.value.startsWith('OP')) {
|
||||
@@ -477,6 +616,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If validation passed and scan-to-boxes is enabled, intercept submission
|
||||
if (!hasError && scanToBoxesEnabled) {
|
||||
console.log('Validation passed, intercepting for scan-to-boxes');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await submitScanWithBoxAssignment();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Add functionality for clear saved codes button
|
||||
@@ -501,6 +649,65 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Functions for scan-to-boxes feature
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Quick box creation button
|
||||
document.getElementById('quick-box-create-btn').addEventListener('click', async function() {
|
||||
try {
|
||||
const createResponse = await fetch('/warehouse/manage_boxes', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
body: 'action=add_box'
|
||||
});
|
||||
|
||||
if (!createResponse.ok) throw new Error('Failed to create box');
|
||||
|
||||
const result = await createResponse.json();
|
||||
|
||||
if (result.success && result.box_number) {
|
||||
await assignCpToBox(result.box_number);
|
||||
showNotification(`✅ Box ${result.box_number} created and CP assigned!`, 'success');
|
||||
closeBoxModal();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to create box');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('❌ Error creating box: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Assign to scanned box button
|
||||
document.getElementById('assign-to-box-btn').addEventListener('click', async function() {
|
||||
const boxNumber = document.getElementById('scan-box-input').value.trim();
|
||||
if (!boxNumber) {
|
||||
showNotification('⚠️ Please scan or enter a box number', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await assignCpToBox(boxNumber);
|
||||
showNotification(`✅ CP ${currentCpCode} assigned to box ${boxNumber}`, 'success');
|
||||
setTimeout(() => closeBoxModal(), 1000);
|
||||
} catch (error) {
|
||||
showNotification('❌ Error: ' + error.message, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('box-assignment-modal');
|
||||
if (event.target == modal) {
|
||||
closeBoxModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="scan-container">
|
||||
@@ -531,6 +738,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
<button type="submit" class="btn">Submit</button>
|
||||
<button type="button" class="btn" id="clear-saved-btn" style="background-color: #ff6b6b; margin-left: 10px;">Clear Quality Operator</button>
|
||||
|
||||
<!-- Enable/Disable Scan to Boxes Toggle -->
|
||||
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||
<label style="display: flex; align-items: center; justify-content: center; gap: 10px; cursor: pointer;">
|
||||
<span style="font-weight: bold;">Enable Scan to Boxes:</span>
|
||||
<input type="checkbox" id="scan-to-boxes-toggle" style="width: 20px; height: 20px; cursor: pointer;">
|
||||
</label>
|
||||
<p style="font-size: 0.85em; color: #666; text-align: center; margin-top: 8px;">
|
||||
When enabled, good quality scans (000) will prompt for box assignment
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -573,4 +791,118 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Box Assignment Popup Modal -->
|
||||
<div id="box-assignment-modal" class="box-modal" style="display: none;">
|
||||
<div class="box-modal-content">
|
||||
<div class="box-modal-header">
|
||||
<h3>Assign to Box</h3>
|
||||
<span class="box-modal-close" onclick="closeBoxModal()">×</span>
|
||||
</div>
|
||||
<div class="box-modal-body">
|
||||
<p>CP Code: <strong id="modal-cp-code"></strong></p>
|
||||
|
||||
<!-- Quick Box Creation -->
|
||||
<div style="margin: 20px 0; padding: 15px; background: #f0f8ff; border-radius: 5px;">
|
||||
<button type="button" id="quick-box-create-btn" class="btn" style="width: 100%; background: #28a745; color: white;">
|
||||
📦 Quick Box Label Creation
|
||||
</button>
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 8px; text-align: center;">
|
||||
Creates new box and prints label immediately
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin: 15px 0; color: #999;">— OR —</div>
|
||||
|
||||
<!-- Scan Existing Box -->
|
||||
<div style="margin: 20px 0;">
|
||||
<label style="font-weight: bold;">Scan Box Number:</label>
|
||||
<input type="text" id="scan-box-input" placeholder="Scan or enter box number" style="width: 100%; padding: 8px; font-size: 1em; margin-top: 5px;">
|
||||
<p style="font-size: 0.85em; color: #666; margin-top: 5px;">
|
||||
Scan an existing box label to assign this CP code to that box
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="box-modal-buttons" style="margin-top: 20px;">
|
||||
<button type="button" class="btn" onclick="closeBoxModal()" style="background: #6c757d;">Skip</button>
|
||||
<button type="button" id="assign-to-box-btn" class="btn" style="background: #007bff;">Assign to Box</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.box-modal {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.box-modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 10% auto;
|
||||
padding: 0;
|
||||
border: 1px solid #888;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.box-modal-header {
|
||||
padding: 15px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 8px 8px 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.box-modal-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.box-modal-close {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.box-modal-close:hover {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.box-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.box-modal-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
body.dark-mode .box-modal-content {
|
||||
background-color: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
body.dark-mode .box-modal-body {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode #scan-box-input {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -30,7 +30,14 @@
|
||||
<a href="{{ url_for('warehouse.manage_boxes') }}" class="btn">Go to Boxes</a>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Warehouse Reports -->
|
||||
<!-- Card 4: View Inventory -->
|
||||
<div class="dashboard-card">
|
||||
<h3>View Products/Boxes/Locations</h3>
|
||||
<p>Search and view products, boxes, and their warehouse locations.</p>
|
||||
<a href="{{ url_for('warehouse.warehouse_inventory') }}" class="btn">View Inventory</a>
|
||||
</div>
|
||||
|
||||
<!-- Card 5: Warehouse Reports -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Warehouse Reports</h3>
|
||||
<p>View and export warehouse activity and inventory reports.</p>
|
||||
|
||||
348
py_app/app/templates/warehouse_inventory.html
Normal file
348
py_app/app/templates/warehouse_inventory.html
Normal file
@@ -0,0 +1,348 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Warehouse Inventory{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/warehouse.css') }}">
|
||||
<style>
|
||||
.inventory-container {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.inventory-container h2 {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.inventory-table-container h3 {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
body.dark-mode .inventory-container h2 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .inventory-table-container h3 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.search-panel {
|
||||
background: #f8fafc;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.search-panel h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.search-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-field label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #334155;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.search-field input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95em;
|
||||
background: white;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
body.dark-mode .search-panel {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
body.dark-mode .search-panel h3 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .search-field label {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
body.dark-mode .search-field input {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .search-field input::placeholder {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.inventory-table-container {
|
||||
background: #f8fafc;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .inventory-table-container {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.inventory-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.inventory-table th,
|
||||
.inventory-table td {
|
||||
border: 1px solid #cbd5e1;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.inventory-table th {
|
||||
background-color: #e2e8f0;
|
||||
font-weight: bold;
|
||||
color: #334155;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.inventory-table tbody tr:hover {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
|
||||
body.dark-mode .inventory-table th,
|
||||
body.dark-mode .inventory-table td {
|
||||
border: 1px solid #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .inventory-table th {
|
||||
background-color: #0f172a;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
body.dark-mode .inventory-table tbody tr:hover {
|
||||
background-color: #334155;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-open {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
body.dark-mode .no-data {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.summary-stats {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
border-left: 4px solid #007bff;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.stat-card h4 {
|
||||
margin: 0;
|
||||
font-size: 0.85em;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: #1e293b;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
body.dark-mode .stat-card {
|
||||
background: #0f172a;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
body.dark-mode .stat-card h4 {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
body.dark-mode .stat-card .stat-value {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="inventory-container">
|
||||
<h2>Warehouse Inventory - Products/Boxes/Locations</h2>
|
||||
|
||||
<!-- Search Panel -->
|
||||
<div class="search-panel">
|
||||
<h3>🔍 Search Inventory</h3>
|
||||
<form method="GET" class="search-form">
|
||||
<div class="search-field">
|
||||
<label for="search_cp">CP Code:</label>
|
||||
<input type="text" id="search_cp" name="search_cp" value="{{ search_cp }}" placeholder="Search by CP code...">
|
||||
</div>
|
||||
|
||||
<div class="search-field">
|
||||
<label for="search_box">Box Number:</label>
|
||||
<input type="text" id="search_box" name="search_box" value="{{ search_box }}" placeholder="Search by box number...">
|
||||
</div>
|
||||
|
||||
<div class="search-field">
|
||||
<label for="search_location">Location:</label>
|
||||
<input type="text" id="search_location" name="search_location" value="{{ search_location }}" placeholder="Search by location...">
|
||||
</div>
|
||||
|
||||
<div class="search-buttons">
|
||||
<button type="submit" class="btn" style="background: #007bff;">Search</button>
|
||||
<a href="{{ url_for('warehouse.warehouse_inventory') }}" class="btn" style="background: #6c757d;">Clear</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Summary Statistics -->
|
||||
{% if inventory_data %}
|
||||
<div class="summary-stats">
|
||||
<div class="stat-card">
|
||||
<h4>Total Records</h4>
|
||||
<div class="stat-value">{{ inventory_data|length }}</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: #28a745;">
|
||||
<h4>Unique CP Codes</h4>
|
||||
<div class="stat-value">{{ inventory_data|map(attribute=0)|unique|list|length }}</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: #ffc107;">
|
||||
<h4>Unique Boxes</h4>
|
||||
<div class="stat-value">{{ inventory_data|map(attribute=1)|unique|list|length }}</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left-color: #17a2b8;">
|
||||
<h4>Locations Used</h4>
|
||||
<div class="stat-value">{{ inventory_data|map(attribute=2)|select|unique|list|length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Inventory Table -->
|
||||
<div class="inventory-table-container">
|
||||
<h3>Inventory Details</h3>
|
||||
|
||||
{% if inventory_data %}
|
||||
<table class="inventory-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CP Code</th>
|
||||
<th>Box Number</th>
|
||||
<th>Location</th>
|
||||
<th>Scanned At</th>
|
||||
<th>Scanned By</th>
|
||||
<th>Placed At Location</th>
|
||||
<th>Placed By</th>
|
||||
<th>Box Status</th>
|
||||
<th>Location Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in inventory_data %}
|
||||
<tr>
|
||||
<td><strong>{{ row[0] }}</strong></td>
|
||||
<td>{{ row[1] }}</td>
|
||||
<td>{{ row[2] or '<span style="color: #999;">Not assigned</span>'|safe }}</td>
|
||||
<td>{{ row[3].strftime('%Y-%m-%d %H:%M') if row[3] else '' }}</td>
|
||||
<td>{{ row[4] or '' }}</td>
|
||||
<td>{{ row[5].strftime('%Y-%m-%d %H:%M') if row[5] else '' }}</td>
|
||||
<td>{{ row[6] or '' }}</td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ row[7] }}">
|
||||
{{ row[7]|upper if row[7] else 'N/A' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if row[8] %}
|
||||
<span class="status-badge status-{{ row[8] }}">
|
||||
{{ row[8]|upper }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span style="color: #999;">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="no-data">
|
||||
<p>📦 No inventory data found</p>
|
||||
{% if search_cp or search_box or search_location %}
|
||||
<p>Try adjusting your search criteria</p>
|
||||
{% else %}
|
||||
<p>Start scanning products to boxes to populate inventory</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -68,6 +68,64 @@ def ensure_boxes_crates_table():
|
||||
except Exception as e:
|
||||
print(f"Error ensuring boxes_crates table: {e}")
|
||||
|
||||
def ensure_box_contents_table():
|
||||
"""Ensure box_contents table exists for tracking CP codes in boxes"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SHOW TABLES LIKE 'box_contents'")
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS box_contents (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
box_id BIGINT NOT NULL,
|
||||
cp_code VARCHAR(15) NOT NULL,
|
||||
scan_id BIGINT,
|
||||
scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
scanned_by VARCHAR(100),
|
||||
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||
INDEX idx_box_id (box_id),
|
||||
INDEX idx_cp_code (cp_code)
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
print("box_contents table created successfully")
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Error ensuring box_contents table: {e}")
|
||||
|
||||
def ensure_location_contents_table():
|
||||
"""Ensure location_contents table exists for tracking boxes in locations"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SHOW TABLES LIKE 'location_contents'")
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS location_contents (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
location_id BIGINT NOT NULL,
|
||||
box_id BIGINT NOT NULL,
|
||||
placed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
placed_by VARCHAR(100),
|
||||
removed_at TIMESTAMP NULL,
|
||||
removed_by VARCHAR(100),
|
||||
status ENUM('active', 'removed') DEFAULT 'active',
|
||||
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||
INDEX idx_location_id (location_id),
|
||||
INDEX idx_box_id (box_id),
|
||||
INDEX idx_status (status)
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
print("location_contents table created successfully")
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Error ensuring location_contents table: {e}")
|
||||
|
||||
# Add warehouse-specific functions below
|
||||
def add_location(location_code, size, description):
|
||||
conn = get_db_connection()
|
||||
@@ -418,6 +476,17 @@ def manage_boxes_handler():
|
||||
elif action == "add_box":
|
||||
created_by = session.get('user', 'Unknown')
|
||||
message = add_box(None, created_by) # Create box without location
|
||||
|
||||
# Check if this is an AJAX request
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
# Extract box number from message (format: "Box 12345678 created successfully")
|
||||
import re
|
||||
match = re.search(r'Box (\d{8})', message)
|
||||
if match:
|
||||
return jsonify({'success': True, 'box_number': match.group(1), 'message': message})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': message})
|
||||
|
||||
session['flash_message'] = message
|
||||
elif action == "update_status":
|
||||
box_id = request.form.get("box_id")
|
||||
@@ -467,3 +536,127 @@ def delete_location_by_id(location_id):
|
||||
except Exception as e:
|
||||
print(f"Error deleting location: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def assign_cp_to_box_handler():
|
||||
"""Handle assigning CP code to a box"""
|
||||
from flask import request, jsonify, session
|
||||
import json
|
||||
|
||||
try:
|
||||
# Ensure box_contents table exists
|
||||
ensure_box_contents_table()
|
||||
|
||||
data = json.loads(request.data)
|
||||
box_number = data.get('box_number')
|
||||
cp_code = data.get('cp_code')
|
||||
scanned_by = session.get('user', 'Unknown')
|
||||
|
||||
if not box_number or not cp_code:
|
||||
return jsonify({'success': False, 'error': 'Missing box_number or cp_code'}), 400
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Find the box by number
|
||||
cursor.execute("SELECT id FROM boxes_crates WHERE box_number = %s", (box_number,))
|
||||
box = cursor.fetchone()
|
||||
|
||||
if not box:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': f'Box {box_number} not found'}), 404
|
||||
|
||||
box_id = box[0]
|
||||
|
||||
# Insert into box_contents
|
||||
cursor.execute("""
|
||||
INSERT INTO box_contents (box_id, cp_code, scanned_by)
|
||||
VALUES (%s, %s, %s)
|
||||
""", (box_id, cp_code, scanned_by))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'CP {cp_code} assigned to box {box_number}'
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"Error in assign_cp_to_box_handler: {e}")
|
||||
print(traceback.format_exc())
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
def view_warehouse_inventory_handler():
|
||||
"""Handle warehouse inventory view - shows CP codes, boxes, and locations"""
|
||||
from flask import render_template, request
|
||||
|
||||
try:
|
||||
# Ensure tables exist
|
||||
ensure_box_contents_table()
|
||||
ensure_location_contents_table()
|
||||
|
||||
# Get search parameters
|
||||
search_cp = request.args.get('search_cp', '').strip()
|
||||
search_box = request.args.get('search_box', '').strip()
|
||||
search_location = request.args.get('search_location', '').strip()
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build query with joins and filters
|
||||
query = """
|
||||
SELECT
|
||||
bc.cp_code,
|
||||
b.box_number,
|
||||
wl.location_code,
|
||||
bc.scanned_at,
|
||||
bc.scanned_by,
|
||||
lc.placed_at,
|
||||
lc.placed_by,
|
||||
b.status as box_status,
|
||||
lc.status as location_status
|
||||
FROM box_contents bc
|
||||
INNER JOIN boxes_crates b ON bc.box_id = b.id
|
||||
LEFT JOIN location_contents lc ON b.id = lc.box_id AND lc.status = 'active'
|
||||
LEFT JOIN warehouse_locations wl ON lc.location_id = wl.id
|
||||
WHERE 1=1
|
||||
"""
|
||||
|
||||
params = []
|
||||
|
||||
if search_cp:
|
||||
query += " AND bc.cp_code LIKE %s"
|
||||
params.append(f"%{search_cp}%")
|
||||
|
||||
if search_box:
|
||||
query += " AND b.box_number LIKE %s"
|
||||
params.append(f"%{search_box}%")
|
||||
|
||||
if search_location:
|
||||
query += " AND wl.location_code LIKE %s"
|
||||
params.append(f"%{search_location}%")
|
||||
|
||||
query += " ORDER BY bc.scanned_at DESC"
|
||||
|
||||
cursor.execute(query, params)
|
||||
inventory_data = cursor.fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
return render_template(
|
||||
'warehouse_inventory.html',
|
||||
inventory_data=inventory_data,
|
||||
search_cp=search_cp,
|
||||
search_box=search_box,
|
||||
search_location=search_location
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
print(f"Error in view_warehouse_inventory_handler: {e}")
|
||||
print(error_trace)
|
||||
return f"<h1>Error loading warehouse inventory</h1><pre>{error_trace}</pre>", 500
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user