Add boxes/crates management system to warehouse module
- Created boxes_crates table with 8-digit auto-generated box numbers - Added manage_boxes route and full CRUD operations - Implemented box status tracking (open/closed) - Added location assignment functionality - Integrated QZ Tray printing with barcode labels - Created manage_boxes.html with table, preview, and print features - Added 'Manage Boxes/Crates' card to warehouse main page - Boxes can be created without location and assigned later - Includes delete functionality for admin/management roles - Added box statistics display
This commit is contained in:
@@ -184,6 +184,38 @@ def create_warehouse_locations_table():
|
||||
print_error(f"Failed to create warehouse_locations table: {e}")
|
||||
return False
|
||||
|
||||
def create_boxes_crates_table():
|
||||
"""Create boxes_crates table for tracking boxes containing scanned orders"""
|
||||
print_step(5, "Creating Boxes/Crates Table")
|
||||
|
||||
try:
|
||||
conn = mariadb.connect(**DB_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
|
||||
boxes_query = """
|
||||
CREATE TABLE IF NOT EXISTS boxes_crates (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
box_number VARCHAR(8) NOT NULL UNIQUE,
|
||||
status ENUM('open', 'closed') DEFAULT 'open',
|
||||
location_id BIGINT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100),
|
||||
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
|
||||
);
|
||||
"""
|
||||
cursor.execute(boxes_query)
|
||||
print_success("Table 'boxes_crates' created successfully")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"Failed to create boxes_crates table: {e}")
|
||||
return False
|
||||
|
||||
def create_permissions_tables():
|
||||
"""Create permission management tables"""
|
||||
print_step(5, "Creating Permission Management Tables")
|
||||
@@ -705,6 +737,7 @@ def main():
|
||||
create_scan_tables,
|
||||
create_order_for_labels_table,
|
||||
create_warehouse_locations_table,
|
||||
create_boxes_crates_table,
|
||||
create_permissions_tables,
|
||||
create_users_table_mariadb,
|
||||
# create_sqlite_tables, # Disabled - using MariaDB only
|
||||
|
||||
@@ -3940,6 +3940,13 @@ def delete_location():
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@warehouse_bp.route('/manage_boxes', methods=['GET', 'POST'])
|
||||
@requires_warehouse_module
|
||||
def manage_boxes():
|
||||
from app.warehouse import manage_boxes_handler
|
||||
return manage_boxes_handler()
|
||||
|
||||
|
||||
# Daily Mirror Route Redirects for Backward Compatibility
|
||||
@bp.route('/daily_mirror_main')
|
||||
def daily_mirror_main_route():
|
||||
|
||||
@@ -23,7 +23,14 @@
|
||||
<a href="{{ url_for('warehouse.create_locations') }}" class="btn">Go to Locations</a>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Warehouse Reports -->
|
||||
<!-- Card 3: Manage Boxes/Crates -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Manage Boxes/Crates</h3>
|
||||
<p>Track and manage boxes and crates in the warehouse.</p>
|
||||
<a href="{{ url_for('warehouse.manage_boxes') }}" class="btn">Go to Boxes</a>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Warehouse Reports -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Warehouse Reports</h3>
|
||||
<p>View and export warehouse activity and inventory reports.</p>
|
||||
|
||||
605
py_app/app/templates/manage_boxes.html
Normal file
605
py_app/app/templates/manage_boxes.html
Normal file
@@ -0,0 +1,605 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Manage Boxes{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/warehouse.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.warehouse-page-container .warehouse-container-1 {
|
||||
flex: 0.5; /* Reduced from 1 to 0.5 */
|
||||
}
|
||||
.warehouse-page-container .warehouse-container-2 {
|
||||
flex: 2.5; /* Increased from 2 to 2.5 */
|
||||
}
|
||||
</style>
|
||||
<div class="warehouse-page-container">
|
||||
<!-- Container 1: Add Box (0.5 part width) -->
|
||||
<div class="warehouse-container-1">
|
||||
<h3>Add New Box</h3>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="form-message {{ 'alert-success' if category == 'success' else 'alert-danger' if category == 'error' else 'alert-info' }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<form method="POST" class="form-centered">
|
||||
<input type="hidden" name="action" value="add_box">
|
||||
<div style="font-size: 0.9em; color: #666; margin: 8px 0;">
|
||||
Box number will be auto-generated (8 digits)<br>
|
||||
Location can be assigned later after filling the box.
|
||||
</div>
|
||||
<button type="submit" class="btn">Create Box</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Container 2: Boxes Table (2 parts width) -->
|
||||
<div class="warehouse-container-2">
|
||||
<h3>All Boxes</h3>
|
||||
<div class="warehouse-table-scroll">
|
||||
<table class="scan-table" id="boxes-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Box Number</th>
|
||||
<th>Status</th>
|
||||
<th>Location</th>
|
||||
<th>Created At</th>
|
||||
<th>Created By</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for box in boxes %}
|
||||
<tr class="box-row selectable-row" data-box-id="{{ box[0] }}" data-box-number="{{ box[1] }}">
|
||||
<td>{{ box[0] }}</td>
|
||||
<td><strong>{{ box[1] }}</strong></td>
|
||||
<td>
|
||||
<span class="status-badge status-{{ box[2] }}">{{ box[2]|upper }}</span>
|
||||
</td>
|
||||
<td>{{ box[3] or 'Not assigned' }}</td>
|
||||
<td>{{ box[4].strftime('%Y-%m-%d %H:%M') if box[4] else '' }}</td>
|
||||
<td>{{ box[6] or '' }}</td>
|
||||
<td class="action-cell">
|
||||
<div class="action-buttons">
|
||||
<!-- Status toggle -->
|
||||
<form method="POST" style="display: inline;">
|
||||
<input type="hidden" name="action" value="update_status">
|
||||
<input type="hidden" name="box_id" value="{{ box[0] }}">
|
||||
<input type="hidden" name="new_status" value="{{ 'closed' if box[2] == 'open' else 'open' }}">
|
||||
<button type="submit" class="btn-small btn-info" title="Toggle status">
|
||||
{{ '🔒 Close' if box[2] == 'open' else '🔓 Open' }}
|
||||
</button>
|
||||
</form>
|
||||
<!-- Location change -->
|
||||
<button type="button" class="btn-small btn-warning" onclick="openLocationModal({{ box[0] }}, '{{ box[1] }}', {{ box[7] or 'null' }});" title="Change location">
|
||||
📍 Location
|
||||
</button>
|
||||
<!-- Make label button -->
|
||||
<button type="button" class="btn-small btn-success" onclick="selectAndShowLabel('{{ box[0] }}', '{{ box[1] }}', this);" title="Select for label preview">
|
||||
📋 Make Label
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container 3: Actions and Print (1 part width) -->
|
||||
<div class="warehouse-container-3">
|
||||
<!-- Delete and Stats Section -->
|
||||
<div style="padding: 12px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 16px;">
|
||||
<h4 style="font-size: 1em; margin-bottom: 8px;">Box Statistics</h4>
|
||||
<div style="font-size: 0.85em; display: flex; gap: 12px; margin-bottom: 12px;">
|
||||
<span><strong>Total:</strong> {{ boxes|length }}</span>
|
||||
<span><strong>Open:</strong> {{ boxes|selectattr('2', 'equalto', 'open')|list|length }}</span>
|
||||
<span><strong>Closed:</strong> {{ boxes|selectattr('2', 'equalto', 'closed')|list|length }}</span>
|
||||
</div>
|
||||
|
||||
{% if session['role'] in ['administrator', 'management'] %}
|
||||
<div style="border-top: 1px solid #dee2e6; padding-top: 8px;">
|
||||
<label style="font-weight:bold; font-size: 0.9em;">Delete Boxes</label>
|
||||
<form method="POST" style="display:flex; gap:6px; align-items:center; margin-top: 6px;" onsubmit="return confirmDeleteBoxes();">
|
||||
<input type="hidden" name="action" value="delete_boxes">
|
||||
<input type="text" name="delete_ids" placeholder="e.g. 5,7,12" style="width:120px; font-size: 0.85em; padding: 4px;" required>
|
||||
<button type="submit" class="btn" style="padding:4px 12px; font-size: 0.85em;">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Print Box Label Section -->
|
||||
<div style="padding: 12px; border: 1px solid #dee2e6; border-radius: 5px;">
|
||||
<h3 style="font-size: 1.1em; margin-bottom: 12px;">Print Box Label</h3>
|
||||
<div id="box-selection-hint" style="margin-bottom: 10px; font-size: 11px; color: #666; text-align: center;">
|
||||
Click on a box row in the table to select it for printing
|
||||
</div>
|
||||
<!-- Label Preview -->
|
||||
<div id="box-label-preview" style="display: flex; align-items: center; justify-content: center; min-height: 120px;">
|
||||
<div style="width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
<div class="box-text">
|
||||
<div id="box-number-display" style="font-size: 18px; font-weight: bold;">Select a box</div>
|
||||
</div>
|
||||
<canvas id="box-barcode" style="display: none; margin-top: 10px;"></canvas>
|
||||
<div id="box-barcode-placeholder" style="color: #999; font-style: italic; text-align: center;">No box selected</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Print Controls -->
|
||||
<div style="text-align: center; margin-top: 15px;">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label for="printer-select" style="font-size: 11px; font-weight: 600;">Select Printer:</label>
|
||||
<select id="printer-select" class="form-control form-control-sm" style="width: 200px; margin: 5px auto; font-size: 11px;">
|
||||
<option value="">Loading printers...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="print-box-btn" class="btn btn-success" style="font-size: 12px; padding: 6px 20px;" disabled>
|
||||
🖨️ Print Box Label
|
||||
</button>
|
||||
<button onclick="testQZConnection()" class="btn btn-info" style="font-size: 11px; padding: 4px 12px; margin-left: 8px;">
|
||||
🔧 Test QZ Tray
|
||||
</button>
|
||||
<div id="print-status" style="margin-top: 8px; font-size: 11px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change Location Modal -->
|
||||
<div id="location-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Change Box Location</h3>
|
||||
<span class="close" onclick="closeLocationModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Change location for box: <strong id="modal-box-number"></strong></p>
|
||||
<form method="POST" id="location-form">
|
||||
<input type="hidden" name="action" value="update_location">
|
||||
<input type="hidden" name="box_id" id="modal-box-id">
|
||||
<label>New Location:</label>
|
||||
<select name="new_location_id" id="modal-location-select" required>
|
||||
<option value="">Select a location...</option>
|
||||
{% for loc in locations %}
|
||||
<option value="{{ loc[0] }}">{{ loc[1] }} - {{ loc[3] or 'No description' }}</option>
|
||||
{% endfor %}
|
||||
</select><br>
|
||||
<div class="modal-buttons">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeLocationModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Update Location</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.status-badge {
|
||||
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;
|
||||
}
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn-small {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.85em;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-info {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
.btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
}
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
.btn-info:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
.btn-warning:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
.box-row {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.box-row:hover {
|
||||
background-color: #e2e8f0;
|
||||
}
|
||||
.box-row.selected {
|
||||
background-color: #ffb300 !important;
|
||||
color: #222 !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
.selectable-row {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.selectable-row:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.selectable-row.selected {
|
||||
background-color: #e3f2fd !important;
|
||||
}
|
||||
.modal {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 10% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #888;
|
||||
width: 400px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.close {
|
||||
color: #aaa;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
.close:hover {
|
||||
color: #000;
|
||||
}
|
||||
.modal-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.form-message {
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.alert-error, .alert-danger {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.alert-info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function confirmDeleteBoxes() {
|
||||
return confirm('Do you really want to delete the selected boxes?');
|
||||
}
|
||||
|
||||
function openLocationModal(boxId, boxNumber, currentLocationId) {
|
||||
document.getElementById('modal-box-id').value = boxId;
|
||||
document.getElementById('modal-box-number').textContent = boxNumber;
|
||||
|
||||
const locationSelect = document.getElementById('modal-location-select');
|
||||
if (currentLocationId) {
|
||||
locationSelect.value = currentLocationId;
|
||||
} else {
|
||||
locationSelect.value = '';
|
||||
}
|
||||
|
||||
document.getElementById('location-modal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closeLocationModal() {
|
||||
document.getElementById('location-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
// Close modal when clicking outside of it
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('location-modal');
|
||||
if (event.target == modal) {
|
||||
closeLocationModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Include required JS libraries for printing -->
|
||||
<script src="{{ url_for('static', filename='JsBarcode.all.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='html2canvas.min.js') }}"></script>
|
||||
|
||||
<script>
|
||||
// Box printing functionality
|
||||
let selectedBoxNumber = null;
|
||||
let selectedBoxId = null;
|
||||
|
||||
// Notification system
|
||||
function showNotification(message, type = 'info') {
|
||||
const existingNotifications = document.querySelectorAll('.notification');
|
||||
existingNotifications.forEach(n => n.remove());
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification alert alert-${type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'info'}`;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
max-width: 450px;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
`;
|
||||
|
||||
const formattedMessage = message.replace(/\n/g, '<br>');
|
||||
|
||||
notification.innerHTML = `
|
||||
<div style="display: flex; align-items: flex-start; justify-content: space-between;">
|
||||
<span style="flex: 1; padding-right: 10px; white-space: pre-wrap; font-family: monospace; font-size: 12px;">${formattedMessage}</span>
|
||||
<button type="button" onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; font-size: 20px; cursor: pointer; flex-shrink: 0;">×</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
const timeout = type === 'error' ? 15000 : 5000;
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
// Handle box row selection
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('=== Initializing box selection ===');
|
||||
|
||||
const boxRows = document.querySelectorAll('.box-row');
|
||||
const printButton = document.getElementById('print-box-btn');
|
||||
|
||||
console.log('Found box rows:', boxRows.length);
|
||||
console.log('Box rows array:', Array.from(boxRows));
|
||||
|
||||
if (boxRows.length === 0) {
|
||||
console.error('ERROR: No box rows found! Check if .box-row class exists on tr elements');
|
||||
}
|
||||
|
||||
boxRows.forEach((row, index) => {
|
||||
console.log(`Attaching click handler to row ${index}:`, row);
|
||||
|
||||
row.addEventListener('click', function(e) {
|
||||
console.log('=== ROW CLICKED ===');
|
||||
console.log('Event target:', e.target);
|
||||
console.log('Current target:', e.currentTarget);
|
||||
|
||||
// Don't trigger if clicking on buttons or forms in action cell
|
||||
if (e.target.closest('.action-cell')) {
|
||||
console.log('Clicked in action cell, ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Processing row selection...');
|
||||
|
||||
// Remove selection from other rows
|
||||
boxRows.forEach(r => r.classList.remove('selected'));
|
||||
|
||||
// Select this row
|
||||
this.classList.add('selected');
|
||||
|
||||
// Get box data from data attributes
|
||||
selectedBoxId = this.dataset.boxId;
|
||||
selectedBoxNumber = this.dataset.boxNumber;
|
||||
|
||||
console.log('Selected box ID:', selectedBoxId);
|
||||
console.log('Selected box number:', selectedBoxNumber);
|
||||
|
||||
// Update preview
|
||||
updateBoxPreview();
|
||||
});
|
||||
});
|
||||
|
||||
console.log('=== Box selection initialized ===');
|
||||
|
||||
// Initialize QZ Tray
|
||||
initQZTray();
|
||||
});
|
||||
|
||||
function updateBoxPreview() {
|
||||
if (selectedBoxNumber) {
|
||||
document.getElementById('box-number-display').textContent = selectedBoxNumber;
|
||||
document.getElementById('box-barcode-placeholder').style.display = 'none';
|
||||
|
||||
const canvas = document.getElementById('box-barcode');
|
||||
canvas.style.display = 'block';
|
||||
|
||||
// Generate barcode
|
||||
JsBarcode(canvas, selectedBoxNumber, {
|
||||
format: "CODE128",
|
||||
width: 2,
|
||||
height: 50,
|
||||
displayValue: true,
|
||||
fontSize: 14
|
||||
});
|
||||
|
||||
document.getElementById('print-box-btn').disabled = false;
|
||||
} else {
|
||||
document.getElementById('box-number-display').textContent = 'Select a box';
|
||||
document.getElementById('box-barcode-placeholder').style.display = 'block';
|
||||
document.getElementById('box-barcode').style.display = 'none';
|
||||
document.getElementById('print-box-btn').disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// QZ Tray functionality
|
||||
async function initQZTray() {
|
||||
try {
|
||||
if (!window.qz) {
|
||||
console.error('QZ Tray library not loaded');
|
||||
document.getElementById('print-status').innerHTML = '<span style="color: red;">QZ Tray library not loaded</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.qz.websocket.isActive()) {
|
||||
await window.qz.websocket.connect();
|
||||
}
|
||||
await loadPrinters();
|
||||
} catch (e) {
|
||||
console.error('QZ Tray initialization failed:', e);
|
||||
document.getElementById('print-status').innerHTML = '<span style="color: red;">QZ Tray not connected</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPrinters() {
|
||||
try {
|
||||
const printers = await window.qz.printers.find();
|
||||
const printerSelect = document.getElementById('printer-select');
|
||||
printerSelect.innerHTML = '';
|
||||
|
||||
if (printers.length === 0) {
|
||||
printerSelect.innerHTML = '<option value="">No printers found</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
printers.forEach(printer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = printer;
|
||||
option.textContent = printer;
|
||||
printerSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Select first printer by default
|
||||
if (printers.length > 0) {
|
||||
printerSelect.value = printers[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading printers:', e);
|
||||
showNotification('Error loading printers: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function testQZConnection() {
|
||||
try {
|
||||
if (!window.qz.websocket.isActive()) {
|
||||
await window.qz.websocket.connect();
|
||||
}
|
||||
showNotification('QZ Tray connected successfully!', 'success');
|
||||
await loadPrinters();
|
||||
} catch (e) {
|
||||
showNotification('QZ Tray connection failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Function to select box and show label preview from action button
|
||||
function selectAndShowLabel(boxId, boxNumber, buttonElement) {
|
||||
console.log('Make Label button clicked for box:', boxId, boxNumber);
|
||||
|
||||
// Find the row that contains this button
|
||||
const row = buttonElement.closest('tr.box-row');
|
||||
|
||||
if (!row) {
|
||||
console.error('Could not find parent row');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove selection from all rows
|
||||
document.querySelectorAll('.box-row').forEach(r => {
|
||||
r.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Select this row
|
||||
row.classList.add('selected');
|
||||
|
||||
// Update selected box data
|
||||
selectedBoxId = boxId;
|
||||
selectedBoxNumber = boxNumber;
|
||||
|
||||
console.log('Box selected for label:', selectedBoxId, selectedBoxNumber);
|
||||
|
||||
// Update preview
|
||||
updateBoxPreview();
|
||||
|
||||
// Scroll to print preview section
|
||||
document.getElementById('box-label-preview').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
document.getElementById('print-box-btn').addEventListener('click', async function() {
|
||||
if (!selectedBoxNumber) {
|
||||
showNotification('Please select a box from the table', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const printerSelect = document.getElementById('printer-select');
|
||||
const selectedPrinter = printerSelect.value;
|
||||
|
||||
if (!selectedPrinter) {
|
||||
showNotification('Please select a printer', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
document.getElementById('print-status').innerHTML = '<span style="color: blue;">Printing...</span>';
|
||||
|
||||
// Create ZPL code for box label
|
||||
const zpl = `^XA
|
||||
^FO50,50^A0N,40,40^FDBox: ${selectedBoxNumber}^FS
|
||||
^FO50,120^BY2,3,80^BCN,80,Y,N,N^FD${selectedBoxNumber}^FS
|
||||
^XZ`;
|
||||
|
||||
const config = window.qz.configs.create(selectedPrinter);
|
||||
await window.qz.print(config, [zpl]);
|
||||
|
||||
showNotification('Box label printed successfully!', 'success');
|
||||
document.getElementById('print-status').innerHTML = '<span style="color: green;">✓ Printed successfully</span>';
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('print-status').innerHTML = '';
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
console.error('Print error:', e);
|
||||
showNotification('Print failed: ' + e.message, 'error');
|
||||
document.getElementById('print-status').innerHTML = '<span style="color: red;">Print failed</span>';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -44,6 +44,30 @@ def ensure_warehouse_locations_table():
|
||||
except Exception as e:
|
||||
print(f"Error ensuring warehouse_locations table: {e}")
|
||||
|
||||
def ensure_boxes_crates_table():
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SHOW TABLES LIKE 'boxes_crates'")
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS boxes_crates (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
box_number VARCHAR(8) NOT NULL UNIQUE,
|
||||
status ENUM('open', 'closed') DEFAULT 'open',
|
||||
location_id BIGINT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100),
|
||||
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Error ensuring boxes_crates table: {e}")
|
||||
|
||||
# Add warehouse-specific functions below
|
||||
def add_location(location_code, size, description):
|
||||
conn = get_db_connection()
|
||||
@@ -268,9 +292,158 @@ def update_location(location_id, location_code, size, description):
|
||||
except Exception as e:
|
||||
print(f"Error updating location: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
print(f"Error updating location: {e}")
|
||||
|
||||
# ============================================================================
|
||||
# Boxes/Crates Functions
|
||||
# ============================================================================
|
||||
|
||||
def generate_box_number():
|
||||
"""Generate next box number with 8 digits (00000001, 00000002, etc.)"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT MAX(CAST(box_number AS UNSIGNED)) FROM boxes_crates")
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result and result[0]:
|
||||
next_number = int(result[0]) + 1
|
||||
else:
|
||||
next_number = 1
|
||||
|
||||
return str(next_number).zfill(8)
|
||||
|
||||
def add_box(location_id=None, created_by=None):
|
||||
"""Add a new box/crate"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
box_number = generate_box_number()
|
||||
|
||||
try:
|
||||
cursor.execute(
|
||||
"INSERT INTO boxes_crates (box_number, status, location_id, created_by) VALUES (%s, %s, %s, %s)",
|
||||
(box_number, 'open', location_id if location_id else None, created_by)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return f"Box {box_number} created successfully"
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return f"Error creating box: {e}"
|
||||
|
||||
def get_boxes():
|
||||
"""Get all boxes with location information"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
b.id,
|
||||
b.box_number,
|
||||
b.status,
|
||||
l.location_code,
|
||||
b.created_at,
|
||||
b.updated_at,
|
||||
b.created_by,
|
||||
b.location_id
|
||||
FROM boxes_crates b
|
||||
LEFT JOIN warehouse_locations l ON b.location_id = l.id
|
||||
ORDER BY b.id DESC
|
||||
""")
|
||||
boxes = cursor.fetchall()
|
||||
conn.close()
|
||||
return boxes
|
||||
|
||||
def update_box_status(box_id, status):
|
||||
"""Update box status (open/closed)"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"UPDATE boxes_crates SET status = %s WHERE id = %s",
|
||||
(status, box_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"success": True, "message": f"Box status updated to {status}"}
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def update_box_location(box_id, location_id):
|
||||
"""Update box location"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute(
|
||||
"UPDATE boxes_crates SET location_id = %s WHERE id = %s",
|
||||
(location_id if location_id else None, box_id)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return {"success": True, "message": "Box location updated"}
|
||||
except Exception as e:
|
||||
conn.close()
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def delete_boxes_by_ids(ids_str):
|
||||
"""Delete boxes by comma-separated IDs"""
|
||||
ids = [id.strip() for id in ids_str.split(',') if id.strip().isdigit()]
|
||||
if not ids:
|
||||
return "No valid IDs provided."
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
deleted = 0
|
||||
for id in ids:
|
||||
cursor.execute("DELETE FROM boxes_crates WHERE id = %s", (id,))
|
||||
if cursor.rowcount:
|
||||
deleted += 1
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return f"Deleted {deleted} box(es)."
|
||||
|
||||
def manage_boxes_handler():
|
||||
"""Handler for boxes/crates management page"""
|
||||
try:
|
||||
# Ensure table exists
|
||||
ensure_boxes_crates_table()
|
||||
ensure_warehouse_locations_table()
|
||||
|
||||
if request.method == "POST":
|
||||
action = request.form.get("action")
|
||||
|
||||
if action == "delete_boxes":
|
||||
ids_str = request.form.get("delete_ids", "")
|
||||
message = delete_boxes_by_ids(ids_str)
|
||||
session['flash_message'] = message
|
||||
elif action == "add_box":
|
||||
created_by = session.get('user', 'Unknown')
|
||||
message = add_box(None, created_by) # Create box without location
|
||||
session['flash_message'] = message
|
||||
elif action == "update_status":
|
||||
box_id = request.form.get("box_id")
|
||||
new_status = request.form.get("new_status")
|
||||
message = update_box_status(box_id, new_status)
|
||||
session['flash_message'] = message
|
||||
elif action == "update_location":
|
||||
box_id = request.form.get("box_id")
|
||||
new_location_id = request.form.get("new_location_id")
|
||||
message = update_box_location(box_id, new_location_id)
|
||||
session['flash_message'] = message
|
||||
|
||||
return redirect(url_for('warehouse.manage_boxes'))
|
||||
|
||||
# Get flash message from session if any
|
||||
message = session.pop('flash_message', None)
|
||||
boxes = get_boxes()
|
||||
locations = get_locations()
|
||||
return render_template("manage_boxes.html", boxes=boxes, locations=locations, message=message)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
print(f"Error in manage_boxes_handler: {e}")
|
||||
print(error_trace)
|
||||
return f"<h1>Error loading boxes management</h1><pre>{error_trace}</pre>", 500
|
||||
|
||||
def delete_location_by_id(location_id):
|
||||
"""Delete a warehouse location by ID"""
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user