diff --git a/backups/backups_metadata.json b/backups/backups_metadata.json index 9934c71..8b8b467 100644 --- a/backups/backups_metadata.json +++ b/backups/backups_metadata.json @@ -118,5 +118,11 @@ "size": 539493, "timestamp": "2025-11-22T03:00:00.182628", "database": "trasabilitate" + }, + { + "filename": "data_only_scheduled_20251227_030000.sql", + "size": 16038, + "timestamp": "2025-12-27T03:00:00.088164", + "database": "trasabilitate" } ] \ No newline at end of file diff --git a/py_app/app/pdf_generator.py b/py_app/app/pdf_generator.py index 42e218f..fd769dc 100644 --- a/py_app/app/pdf_generator.py +++ b/py_app/app/pdf_generator.py @@ -159,10 +159,10 @@ class LabelPDFGenerator: # Calculate row positions from top current_y = self.content_y + self.content_height - # Row 1: Company Header - "INNOFA RROMANIA SRL" + # Row 1: Company Header - (removed) row_y = current_y - self.row_height canvas.setFont("Helvetica-Bold", 10) - text = "INNOFA RROMANIA SRL" + text = "" text_width = canvas.stringWidth(text, "Helvetica-Bold", 10) x_centered = self.content_x + (self.content_width - text_width) / 2 canvas.drawString(x_centered, row_y + self.row_height/3, text) diff --git a/py_app/app/routes.py b/py_app/app/routes.py index 15ca5f5..8f37284 100644 --- a/py_app/app/routes.py +++ b/py_app/app/routes.py @@ -6,7 +6,14 @@ from flask import Blueprint, render_template, redirect, url_for, request, flash, from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas import csv -from .warehouse import add_location +from .warehouse import ( + add_location, + search_box_by_number, + assign_box_to_location, + search_location_with_boxes, + move_box_to_new_location, + change_box_status +) from app.settings import ( settings_handler, role_permissions_handler, @@ -3710,6 +3717,133 @@ def generate_labels_pdf(order_id, paper_saving_mode='true'): return jsonify({'error': str(e)}), 500 +@bp.route('/generate_box_label_pdf', methods=['POST']) +def generate_box_label_pdf(): + """Generate a PDF box label with barcode for printing via QZ Tray""" + + if 'role' not in session: + return jsonify({'error': 'Access denied. Please log in.'}), 403 + + try: + from io import BytesIO + from reportlab.lib.pagesizes import letter + from reportlab.pdfgen import canvas + from reportlab.lib import colors + from reportlab.lib.units import mm + from reportlab.graphics.barcode import code128 + from flask import make_response + + # Get box number from request + box_number = request.form.get('box_number', 'Unknown') + + print(f"DEBUG: Generating box label PDF for box: {box_number}") + + # Create PDF buffer + pdf_buffer = BytesIO() + + # Create canvas with 8cm x 5cm size in landscape orientation + page_width = 80 * mm # 8 cm + page_height = 50 * mm # 5 cm + c = canvas.Canvas(pdf_buffer, pagesize=(page_width, page_height)) + + # Optimize for label printer + c.setPageCompression(1) + c.setCreator("Trasabilitate Box Label System") + c.setTitle("Box Label - Optimized for Label Printers") + + # Define margins and usable area + margin = 2 * mm + usable_width = page_width - (2 * margin) + usable_height = page_height - (2 * margin) + + # Calculate vertical layout + # Top section: "BOX Nr: XXXXXXXX" text + # Bottom section: Barcode + text_height = 12 * mm # Space for text at top (reduced from 15mm) + barcode_height = usable_height - text_height - (1 * mm) # Rest for barcode with minimal spacing (reduced from 3mm) + + # === TOP SECTION: BOX Nr Label === + # Position from top of usable area + text_y = page_height - margin - text_height + + # Draw "BOX Nr:" label + c.setFont("Helvetica-Bold", 14) + label_text = "BOX Nr:" + label_width = c.stringWidth(label_text, "Helvetica-Bold", 14) + + # Draw box number + c.setFont("Helvetica-Bold", 18) + number_text = box_number + number_width = c.stringWidth(number_text, "Helvetica-Bold", 18) + + # Calculate total width and center everything + total_text_width = label_width + 3*mm + number_width # 3mm spacing between label and number + start_x = margin + (usable_width - total_text_width) / 2 + + # Draw label text + c.setFont("Helvetica-Bold", 14) + c.drawString(start_x, text_y + 5*mm, label_text) + + # Draw box number + c.setFont("Helvetica-Bold", 18) + c.drawString(start_x + label_width + 3*mm, text_y + 5*mm, number_text) + + # === BOTTOM SECTION: Barcode === + barcode_y = margin + + try: + # Create barcode for box number + barcode = code128.Code128( + box_number, + barWidth=0.4*mm, # Thicker bars for better scanning + barHeight=barcode_height, + humanReadable=True, + fontSize=10 + ) + + # Calculate scaling to fit width + scale_factor = usable_width / barcode.width + + # Center the barcode horizontally + barcode_x = margin + (usable_width - (barcode.width * scale_factor)) / 2 + + # Draw the barcode + c.saveState() + c.translate(barcode_x, barcode_y) + c.scale(scale_factor, 1) + barcode.drawOn(c, 0, 0) + c.restoreState() + + print(f"DEBUG: Barcode generated successfully with scale factor: {scale_factor}") + + except Exception as e: + print(f"DEBUG: Error generating barcode: {e}") + # Fallback: draw text if barcode fails + c.setFont("Helvetica-Bold", 12) + text_width = c.stringWidth(box_number, "Helvetica-Bold", 12) + c.drawString((page_width - text_width) / 2, barcode_y + barcode_height/2, box_number) + + c.save() + + # Get PDF data + pdf_buffer.seek(0) + pdf_data = pdf_buffer.getvalue() + + # Create response + response = make_response(pdf_data) + response.headers['Content-Type'] = 'application/pdf' + response.headers['Content-Disposition'] = f'inline; filename="box_label_{box_number}.pdf"' + + print(f"DEBUG: Box label PDF generated successfully") + return response + + except Exception as e: + print(f"DEBUG: Error generating box label PDF: {e}") + import traceback + traceback.print_exc() + return jsonify({'error': str(e)}), 500 + + @bp.route('/generate_label_pdf', methods=['POST']) def generate_label_pdf(): """Generate a single label PDF for thermal printing via QZ Tray""" @@ -5346,3 +5480,98 @@ def restore_single_table(): }), 500 +# Warehouse Box Location Management API Routes + +@bp.route('/api/warehouse/box/search', methods=['POST']) +@requires_warehouse_module +def api_search_box(): + """Search for a box by box number""" + data = request.get_json() + box_number = data.get('box_number', '').strip() + + success, response_data, status_code = search_box_by_number(box_number) + + return jsonify({ + 'success': success, + **response_data + }), status_code + + +@bp.route('/api/warehouse/box/assign-location', methods=['POST']) +@requires_warehouse_module +def api_assign_box_to_location(): + """Assign a box to a warehouse location (only if box is closed)""" + data = request.get_json() + box_id = data.get('box_id') + location_code = data.get('location_code', '').strip() + + # Additional check: verify box is closed before assigning + if box_id: + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT status FROM boxes_crates WHERE id = %s", (box_id,)) + result = cursor.fetchone() + conn.close() + + if result and result[0] == 'open': + return jsonify({ + 'success': False, + 'message': 'Cannot assign an open box to a location. Please close the box first.' + }), 400 + except Exception as e: + pass # Continue to the main function which will handle other errors + + success, response_data, status_code = assign_box_to_location(box_id, location_code) + + return jsonify({ + 'success': success, + **response_data + }), status_code + + +@bp.route('/api/warehouse/box/change-status', methods=['POST']) +@requires_warehouse_module +def api_change_box_status(): + """Change the status of a box (open/closed)""" + data = request.get_json() + box_id = data.get('box_id') + new_status = data.get('new_status', '').strip() + + success, response_data, status_code = change_box_status(box_id, new_status) + + return jsonify({ + 'success': success, + **response_data + }), status_code + + +@bp.route('/api/warehouse/location/search', methods=['POST']) +@requires_warehouse_module +def api_search_location(): + """Search for a location and get all boxes in it""" + data = request.get_json() + location_code = data.get('location_code', '').strip() + + success, response_data, status_code = search_location_with_boxes(location_code) + + return jsonify({ + 'success': success, + **response_data + }), status_code + + +@bp.route('/api/warehouse/box/move-location', methods=['POST']) +@requires_warehouse_module +def api_move_box_to_location(): + """Move a box from one location to another""" + data = request.get_json() + box_id = data.get('box_id') + new_location_code = data.get('new_location_code', '').strip() + + success, response_data, status_code = move_box_to_new_location(box_id, new_location_code) + + return jsonify({ + 'success': success, + **response_data + }), status_code diff --git a/py_app/app/templates/base.html b/py_app/app/templates/base.html index c544c79..9a52421 100644 --- a/py_app/app/templates/base.html +++ b/py_app/app/templates/base.html @@ -50,7 +50,7 @@ {% if request.endpoint in ['main.etichete', 'main.upload_data', 'main.view_orders', 'main.print_module', 'main.print_lost_labels'] %} Labels Module {% endif %} - {% if request.endpoint.startswith('warehouse.') %} + {% if request.endpoint.startswith('warehouse.') or request.endpoint in ['main.store_articles', 'main.warehouse_reports'] %} Warehouse Main {% endif %} Go to Dashboard diff --git a/py_app/app/templates/create_locations.html b/py_app/app/templates/create_locations.html index 9e23c72..c02bf98 100644 --- a/py_app/app/templates/create_locations.html +++ b/py_app/app/templates/create_locations.html @@ -510,11 +510,13 @@ async function printLocationBarcode() { printStatus.textContent = 'Sending to printer...'; - // Configure QZ Tray for PDF printing with 4x8cm size + // Configure QZ Tray for PDF printing with 8x5cm size in landscape const config = qzTray.configs.create(selectedPrinter, { - size: { width: 4, height: 8, units: 'cm' }, - margins: { top: 0, right: 0, bottom: 0, left: 0 }, - orientation: 'portrait' + scaleContent: false, + rasterize: false, + size: { width: 80, height: 50 }, + units: 'mm', + margins: { top: 0, right: 0, bottom: 0, left: 0 } }); const data = [{ diff --git a/py_app/app/templates/fg_scan.html b/py_app/app/templates/fg_scan.html index a66fb47..7255b7e 100644 --- a/py_app/app/templates/fg_scan.html +++ b/py_app/app/templates/fg_scan.html @@ -3,8 +3,8 @@ {% block title %}Finish Good Scan{% endblock %} {% block head %} - - + + + +
+
+ +
+ + +
+ + +
+
+

📦 Scan/Enter Box Number

+ +
+ +
+ + +
+ + + + +
+

Box Details

+
+ Box Number: + - +
+
+ Status: + + - + +
+
+ Current Location: + - +
+ + +
+
+ + + +
+ + +
+
+

📍 Scan/Enter Location Code

+ +
+ +
+ + +
+ + + + +
+

Location Details

+
+ Location Code: + - +
+
+ Boxes Count: + - +
+
+ + +
+

Boxes in This Location

+
+
+
+ + + +
+
+
+ + {% endblock %} diff --git a/py_app/app/warehouse.py b/py_app/app/warehouse.py index 16eda41..e81bf1b 100644 --- a/py_app/app/warehouse.py +++ b/py_app/app/warehouse.py @@ -3,7 +3,7 @@ from flask import current_app, request, render_template, session, redirect, url_ import csv, os, tempfile from reportlab.lib.pagesizes import letter from reportlab.pdfgen import canvas -from reportlab.lib.units import cm +from reportlab.lib.units import cm, mm from reportlab.graphics.barcode import code128 import io @@ -255,7 +255,7 @@ def import_locations_csv_handler(): return render_template('import_locations_csv.html', report=report, locations=locations) def generate_location_label_pdf(): - """Generate PDF for location barcode label (8x4cm)""" + """Generate PDF for location barcode label (8cm x 5cm landscape)""" try: data = request.get_json() location_code = data.get('location_code', '') @@ -263,46 +263,93 @@ def generate_location_label_pdf(): if not location_code: return jsonify({'error': 'Location code is required'}), 400 + print(f"DEBUG: Generating location label PDF for: {location_code}") + # Create PDF in memory buffer = io.BytesIO() - # Create PDF with 8x4cm page size (width x height) - page_width = 8 * cm - page_height = 4 * cm + # Create PDF with 8cm x 5cm page size in landscape orientation + page_width = 80 * mm # 8 cm + page_height = 50 * mm # 5 cm c = canvas.Canvas(buffer, pagesize=(page_width, page_height)) - # Generate Code128 barcode - barcode = code128.Code128(location_code, barWidth=1.0, humanReadable=False) + # Optimize for label printer + c.setPageCompression(1) + c.setCreator("Trasabilitate Location Label System") + c.setTitle("Location Label - Optimized for Label Printers") - # Calculate the desired barcode dimensions (fill most of the label) - desired_barcode_width = 7 * cm # Almost full width - desired_barcode_height = 2.5 * cm # Most of the height + # Define margins and usable area + margin = 2 * mm + usable_width = page_width - (2 * margin) + usable_height = page_height - (2 * margin) - # Calculate scaling factor to fit the desired width - scale = desired_barcode_width / barcode.width + # Calculate vertical layout + # Top section: "Location nr: XXXX" text + # Bottom section: Barcode + text_height = 12 * mm # Space for text at top + barcode_height = usable_height - text_height - (1 * mm) # Rest for barcode with minimal spacing - # Calculate actual dimensions after scaling - actual_width = barcode.width * scale - actual_height = barcode.height * scale + # === TOP SECTION: Location Nr Label === + # Position from top of usable area + text_y = page_height - margin - text_height - # Center the barcode on the label - barcode_x = (page_width - actual_width) / 2 - barcode_y = (page_height - actual_height) / 2 + 0.3 * cm # Slightly above center for text space + # Draw "Location nr:" label + c.setFont("Helvetica-Bold", 14) + label_text = "Location nr:" + label_width = c.stringWidth(label_text, "Helvetica-Bold", 14) - # Draw barcode with scaling - c.saveState() - c.translate(barcode_x, barcode_y) - c.scale(scale, scale) - barcode.drawOn(c, 0, 0) - c.restoreState() + # Draw location code + c.setFont("Helvetica-Bold", 18) + code_text = location_code + code_width = c.stringWidth(code_text, "Helvetica-Bold", 18) - # Add location code text below barcode - c.setFont("Helvetica-Bold", 10) - text_width = c.stringWidth(location_code, "Helvetica-Bold", 10) - text_x = (page_width - text_width) / 2 - text_y = barcode_y - 0.5 * cm # Below the barcode - c.drawString(text_x, text_y, location_code) + # Calculate total width and center everything + total_text_width = label_width + 3*mm + code_width # 3mm spacing between label and code + start_x = margin + (usable_width - total_text_width) / 2 + + # Draw label text + c.setFont("Helvetica-Bold", 14) + c.drawString(start_x, text_y + 5*mm, label_text) + + # Draw location code + c.setFont("Helvetica-Bold", 18) + c.drawString(start_x + label_width + 3*mm, text_y + 5*mm, code_text) + + # === BOTTOM SECTION: Barcode === + barcode_y = margin + + try: + # Create barcode for location code + barcode = code128.Code128( + location_code, + barWidth=0.4*mm, # Thicker bars for better scanning + barHeight=barcode_height, + humanReadable=True, + fontSize=10 + ) + + # Calculate scaling to fit width + scale_factor = usable_width / barcode.width + + # Center the barcode horizontally + barcode_x = margin + (usable_width - (barcode.width * scale_factor)) / 2 + + # Draw the barcode + c.saveState() + c.translate(barcode_x, barcode_y) + c.scale(scale_factor, 1) + barcode.drawOn(c, 0, 0) + c.restoreState() + + print(f"DEBUG: Barcode generated successfully with scale factor: {scale_factor}") + + except Exception as e: + print(f"DEBUG: Error generating barcode: {e}") + # Fallback: draw text if barcode fails + c.setFont("Helvetica-Bold", 12) + text_width = c.stringWidth(location_code, "Helvetica-Bold", 12) + c.drawString((page_width - text_width) / 2, barcode_y + barcode_height/2, location_code) # Finalize PDF c.save() @@ -313,10 +360,13 @@ def generate_location_label_pdf(): response.headers['Content-Type'] = 'application/pdf' response.headers['Content-Disposition'] = f'inline; filename=location_{location_code}_label.pdf' + print(f"DEBUG: Location label PDF generated successfully") return response except Exception as e: print(f"Error generating location label PDF: {e}") + import traceback + traceback.print_exc() return jsonify({'error': str(e)}), 500 def update_location(location_id, location_code, size, description): @@ -660,3 +710,270 @@ def view_warehouse_inventory_handler(): return f"

Error loading warehouse inventory

{error_trace}
", 500 +# Box Location Management Functions + +def search_box_by_number(box_number): + """ + Search for a box by box number and return its details including location + + Args: + box_number (str): The box number to search for + + Returns: + tuple: (success: bool, data: dict, status_code: int) + """ + try: + if not box_number: + return False, {'message': 'Box number is required'}, 400 + + conn = get_db_connection() + cursor = conn.cursor() + + # Search for the box and get its location info + cursor.execute(""" + SELECT + b.id, + b.box_number, + b.status, + b.location_id, + w.location_code + FROM boxes_crates b + LEFT JOIN warehouse_locations w ON b.location_id = w.id + WHERE b.box_number = %s + """, (box_number,)) + + result = cursor.fetchone() + conn.close() + + if result: + return True, { + 'box': { + 'id': result[0], + 'box_number': result[1], + 'status': result[2], + 'location_id': result[3], + 'location_code': result[4] + } + }, 200 + else: + return False, {'message': f'Box "{box_number}" not found in the system'}, 404 + + except Exception as e: + return False, {'message': f'Error searching for box: {str(e)}'}, 500 + + +def assign_box_to_location(box_id, location_code): + """ + Assign a box to a warehouse location + + Args: + box_id (int): The ID of the box to assign + location_code (str): The location code to assign the box to + + Returns: + tuple: (success: bool, data: dict, status_code: int) + """ + try: + if not box_id or not location_code: + return False, {'message': 'Box ID and location code are required'}, 400 + + conn = get_db_connection() + cursor = conn.cursor() + + # Check if location exists + cursor.execute("SELECT id FROM warehouse_locations WHERE location_code = %s", (location_code,)) + location_result = cursor.fetchone() + + if not location_result: + conn.close() + return False, {'message': f'Location "{location_code}" not found in the system'}, 404 + + location_id = location_result[0] + + # Update box location + cursor.execute(""" + UPDATE boxes_crates + SET location_id = %s, updated_at = NOW() + WHERE id = %s + """, (location_id, box_id)) + + conn.commit() + conn.close() + + return True, {'message': f'Box successfully assigned to location "{location_code}"'}, 200 + + except Exception as e: + return False, {'message': f'Error assigning box to location: {str(e)}'}, 500 + + +def search_location_with_boxes(location_code): + """ + Search for a location and get all boxes assigned to it + + Args: + location_code (str): The location code to search for + + Returns: + tuple: (success: bool, data: dict, status_code: int) + """ + try: + if not location_code: + return False, {'message': 'Location code is required'}, 400 + + conn = get_db_connection() + cursor = conn.cursor() + + # Search for the location + cursor.execute(""" + SELECT id, location_code, size, description + FROM warehouse_locations + WHERE location_code = %s + """, (location_code,)) + + location_result = cursor.fetchone() + + if not location_result: + conn.close() + return False, {'message': f'Location "{location_code}" not found in the system'}, 404 + + location_id = location_result[0] + + # Get all boxes assigned to this location + cursor.execute(""" + SELECT + b.id, + b.box_number, + b.status, + b.created_at + FROM boxes_crates b + WHERE b.location_id = %s + ORDER BY b.box_number + """, (location_id,)) + + boxes_results = cursor.fetchall() + conn.close() + + boxes = [] + for box in boxes_results: + boxes.append({ + 'id': box[0], + 'box_number': box[1], + 'status': box[2], + 'created_at': box[3].strftime('%Y-%m-%d %H:%M:%S') if box[3] else None + }) + + return True, { + 'location': { + 'id': location_result[0], + 'location_code': location_result[1], + 'size': location_result[2], + 'description': location_result[3] + }, + 'boxes': boxes + }, 200 + + except Exception as e: + return False, {'message': f'Error searching for location: {str(e)}'}, 500 + + +def move_box_to_new_location(box_id, new_location_code): + """ + Move a box from its current location to a new location + + Args: + box_id (int): The ID of the box to move + new_location_code (str): The new location code + + Returns: + tuple: (success: bool, data: dict, status_code: int) + """ + try: + if not box_id or not new_location_code: + return False, {'message': 'Box ID and new location code are required'}, 400 + + conn = get_db_connection() + cursor = conn.cursor() + + # Check if new location exists + cursor.execute("SELECT id FROM warehouse_locations WHERE location_code = %s", (new_location_code,)) + location_result = cursor.fetchone() + + if not location_result: + conn.close() + return False, {'message': f'Location "{new_location_code}" not found in the system'}, 404 + + new_location_id = location_result[0] + + # Get box number for response message + cursor.execute("SELECT box_number FROM boxes_crates WHERE id = %s", (box_id,)) + box_result = cursor.fetchone() + + if not box_result: + conn.close() + return False, {'message': 'Box not found'}, 404 + + box_number = box_result[0] + + # Update box location + cursor.execute(""" + UPDATE boxes_crates + SET location_id = %s, updated_at = NOW() + WHERE id = %s + """, (new_location_id, box_id)) + + conn.commit() + conn.close() + + return True, {'message': f'Box "{box_number}" successfully moved to location "{new_location_code}"'}, 200 + + except Exception as e: + return False, {'message': f'Error moving box to new location: {str(e)}'}, 500 + + +def change_box_status(box_id, new_status): + """ + Change the status of a box (open/closed) + + Args: + box_id (int): The ID of the box + new_status (str): The new status ('open' or 'closed') + + Returns: + tuple: (success: bool, data: dict, status_code: int) + """ + try: + if not box_id: + return False, {'message': 'Box ID is required'}, 400 + + if new_status not in ['open', 'closed']: + return False, {'message': 'Invalid status. Must be "open" or "closed"'}, 400 + + conn = get_db_connection() + cursor = conn.cursor() + + # Get box number for response message + cursor.execute("SELECT box_number FROM boxes_crates WHERE id = %s", (box_id,)) + box_result = cursor.fetchone() + + if not box_result: + conn.close() + return False, {'message': 'Box not found'}, 404 + + box_number = box_result[0] + + # Update box status + cursor.execute(""" + UPDATE boxes_crates + SET status = %s, updated_at = NOW() + WHERE id = %s + """, (new_status, box_id)) + + conn.commit() + conn.close() + + return True, {'message': f'Box "{box_number}" status changed to "{new_status}"'}, 200 + + except Exception as e: + return False, {'message': f'Error changing box status: {str(e)}'}, 500 + +