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 %} - - + + + +
{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
+
+