updated to uplodad orders
This commit is contained in:
BIN
py_app/app/__pycache__/order_labels.cpython-312.pyc
Normal file
BIN
py_app/app/__pycache__/order_labels.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
151
py_app/app/db_create_scripts/add_printed_labels_column.py
Normal file
151
py_app/app/db_create_scripts/add_printed_labels_column.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database script to add the printed_labels column to the order_for_labels table
|
||||
This column will track whether labels have been printed for each order (boolean: 0=false, 1=true)
|
||||
Default value: 0 (false)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import mariadb
|
||||
from flask import Flask
|
||||
|
||||
# Add the app directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
def get_db_connection():
|
||||
"""Get database connection using settings from external_server.conf"""
|
||||
# Go up two levels from this script to reach py_app directory, then to instance
|
||||
app_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
settings_file = os.path.join(app_root, 'instance', 'external_server.conf')
|
||||
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
key, value = line.strip().split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def add_printed_labels_column():
|
||||
"""
|
||||
Adds the printed_labels column to the order_for_labels table after the line_number column
|
||||
Column type: TINYINT(1) (boolean: 0=false, 1=true)
|
||||
Default value: 0 (false)
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if table exists
|
||||
cursor.execute("SHOW TABLES LIKE 'order_for_labels'")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result:
|
||||
print("❌ Table 'order_for_labels' does not exist. Please create it first.")
|
||||
return False
|
||||
|
||||
# Check if column already exists
|
||||
cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'")
|
||||
column_exists = cursor.fetchone()
|
||||
|
||||
if column_exists:
|
||||
print("ℹ️ Column 'printed_labels' already exists.")
|
||||
# Show current structure
|
||||
cursor.execute("DESCRIBE order_for_labels")
|
||||
columns = cursor.fetchall()
|
||||
print("\n📋 Current table structure:")
|
||||
for col in columns:
|
||||
null_info = 'NULL' if col[2] == 'YES' else 'NOT NULL'
|
||||
default_info = f" DEFAULT {col[4]}" if col[4] else ""
|
||||
print(f" 📌 {col[0]:<25} {col[1]:<20} {null_info}{default_info}")
|
||||
else:
|
||||
# Add the column after line_number
|
||||
alter_table_sql = """
|
||||
ALTER TABLE order_for_labels
|
||||
ADD COLUMN printed_labels TINYINT(1) NOT NULL DEFAULT 0
|
||||
COMMENT 'Boolean flag: 0=labels not printed, 1=labels printed'
|
||||
AFTER line_number
|
||||
"""
|
||||
|
||||
cursor.execute(alter_table_sql)
|
||||
conn.commit()
|
||||
print("✅ Column 'printed_labels' added successfully!")
|
||||
|
||||
# Show the updated structure
|
||||
cursor.execute("DESCRIBE order_for_labels")
|
||||
columns = cursor.fetchall()
|
||||
print("\n📋 Updated table structure:")
|
||||
for col in columns:
|
||||
null_info = 'NULL' if col[2] == 'YES' else 'NOT NULL'
|
||||
default_info = f" DEFAULT {col[4]}" if col[4] else ""
|
||||
highlight = "🆕 " if col[0] == 'printed_labels' else " "
|
||||
print(f"{highlight}{col[0]:<25} {col[1]:<20} {null_info}{default_info}")
|
||||
|
||||
# Show count of existing records that will have printed_labels = 0
|
||||
cursor.execute("SELECT COUNT(*) FROM order_for_labels")
|
||||
count = cursor.fetchone()[0]
|
||||
if count > 0:
|
||||
print(f"\n📊 {count} existing records now have printed_labels = 0 (false)")
|
||||
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"❌ Database error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def verify_column():
|
||||
"""Verify the column was added correctly"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Test the column functionality
|
||||
cursor.execute("SELECT COUNT(*) as total, SUM(printed_labels) as printed FROM order_for_labels")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
total, printed = result
|
||||
print(f"\n🔍 Verification:")
|
||||
print(f" 📦 Total orders: {total}")
|
||||
print(f" 🖨️ Printed orders: {printed or 0}")
|
||||
print(f" 📄 Unprinted orders: {total - (printed or 0)}")
|
||||
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Verification failed: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔧 Adding printed_labels column to order_for_labels table...")
|
||||
print("="*60)
|
||||
|
||||
success = add_printed_labels_column()
|
||||
|
||||
if success:
|
||||
print("\n🔍 Verifying column addition...")
|
||||
verify_column()
|
||||
print("\n✅ Database modification completed successfully!")
|
||||
print("\n📝 Column Details:")
|
||||
print(" • Name: printed_labels")
|
||||
print(" • Type: TINYINT(1) (boolean)")
|
||||
print(" • Default: 0 (false - labels not printed)")
|
||||
print(" • Values: 0 = not printed, 1 = printed")
|
||||
print(" • Position: After line_number column")
|
||||
else:
|
||||
print("\n❌ Database modification failed!")
|
||||
|
||||
print("="*60)
|
||||
110
py_app/app/db_create_scripts/create_order_for_labels_table.py
Normal file
110
py_app/app/db_create_scripts/create_order_for_labels_table.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database script to create the order_for_labels table
|
||||
This table will store order information for label generation
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import mariadb
|
||||
from flask import Flask
|
||||
|
||||
# Add the app directory to the path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
def get_db_connection():
|
||||
"""Get database connection using settings from external_server.conf"""
|
||||
# Go up two levels from this script to reach py_app directory, then to instance
|
||||
app_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
settings_file = os.path.join(app_root, 'instance', 'external_server.conf')
|
||||
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
key, value = line.strip().split('=', 1)
|
||||
settings[key] = value
|
||||
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def create_order_for_labels_table():
|
||||
"""
|
||||
Creates the order_for_labels table with the specified structure
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# First check if table already exists
|
||||
cursor.execute("SHOW TABLES LIKE 'order_for_labels'")
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
print("Table 'order_for_labels' already exists.")
|
||||
# Show current structure
|
||||
cursor.execute("DESCRIBE order_for_labels")
|
||||
columns = cursor.fetchall()
|
||||
print("\nCurrent table structure:")
|
||||
for col in columns:
|
||||
print(f" {col[0]} - {col[1]} {'NULL' if col[2] == 'YES' else 'NOT NULL'}")
|
||||
else:
|
||||
# Create the table
|
||||
create_table_sql = """
|
||||
CREATE TABLE order_for_labels (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Unique identifier',
|
||||
comanda_productie VARCHAR(15) NOT NULL COMMENT 'Production Order',
|
||||
cod_articol VARCHAR(15) COMMENT 'Article Code',
|
||||
descr_com_prod VARCHAR(50) NOT NULL COMMENT 'Production Order Description',
|
||||
cantitate INT(3) NOT NULL COMMENT 'Quantity',
|
||||
com_achiz_client VARCHAR(25) COMMENT 'Client Purchase Order',
|
||||
nr_linie_com_client INT(3) COMMENT 'Client Order Line Number',
|
||||
customer_name VARCHAR(50) COMMENT 'Customer Name',
|
||||
customer_article_number VARCHAR(25) COMMENT 'Customer Article Number',
|
||||
open_for_order VARCHAR(25) COMMENT 'Open for Order Status',
|
||||
line_number INT(3) COMMENT 'Line Number',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Record creation timestamp',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Record update timestamp'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table for storing order information for label generation'
|
||||
"""
|
||||
|
||||
cursor.execute(create_table_sql)
|
||||
conn.commit()
|
||||
print("✅ Table 'order_for_labels' created successfully!")
|
||||
|
||||
# Show the created structure
|
||||
cursor.execute("DESCRIBE order_for_labels")
|
||||
columns = cursor.fetchall()
|
||||
print("\n📋 Table structure:")
|
||||
for col in columns:
|
||||
null_info = 'NULL' if col[2] == 'YES' else 'NOT NULL'
|
||||
default_info = f" DEFAULT {col[4]}" if col[4] else ""
|
||||
print(f" 📌 {col[0]:<25} {col[1]:<20} {null_info}{default_info}")
|
||||
|
||||
conn.close()
|
||||
|
||||
except mariadb.Error as e:
|
||||
print(f"❌ Database error: {e}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🏗️ Creating order_for_labels table...")
|
||||
print("="*50)
|
||||
|
||||
success = create_order_for_labels_table()
|
||||
|
||||
if success:
|
||||
print("\n✅ Database setup completed successfully!")
|
||||
else:
|
||||
print("\n❌ Database setup failed!")
|
||||
|
||||
print("="*50)
|
||||
339
py_app/app/order_labels.py
Normal file
339
py_app/app/order_labels.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
Order Labels Module - Handles CSV upload and processing for order label generation
|
||||
Author: Auto-generated module for order management
|
||||
"""
|
||||
|
||||
import mariadb
|
||||
from flask import current_app, request, render_template, session, redirect, url_for, flash
|
||||
import csv
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
|
||||
def get_db_connection():
|
||||
"""Get database connection using external server configuration"""
|
||||
settings_file = current_app.instance_path + '/external_server.conf'
|
||||
settings = {}
|
||||
with open(settings_file, 'r') as f:
|
||||
for line in f:
|
||||
key, value = line.strip().split('=', 1)
|
||||
settings[key] = value
|
||||
return mariadb.connect(
|
||||
user=settings['username'],
|
||||
password=settings['password'],
|
||||
host=settings['server_domain'],
|
||||
port=int(settings['port']),
|
||||
database=settings['database_name']
|
||||
)
|
||||
|
||||
def validate_order_row(row_data):
|
||||
"""
|
||||
Validate a single order row for required fields and data types
|
||||
Required fields: Comanda Productie, Cantitate, Descr. Com. Prod
|
||||
"""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Check required fields
|
||||
if not row_data.get('comanda_productie', '').strip():
|
||||
errors.append("Comanda Productie is required")
|
||||
|
||||
if not row_data.get('descr_com_prod', '').strip():
|
||||
errors.append("Descr. Com. Prod is required")
|
||||
|
||||
# Validate Cantitate (quantity) - must be integer
|
||||
cantitate_str = row_data.get('cantitate', '').strip()
|
||||
if not cantitate_str:
|
||||
errors.append("Cantitate is required")
|
||||
else:
|
||||
try:
|
||||
cantitate = int(cantitate_str)
|
||||
if cantitate <= 0:
|
||||
errors.append("Cantitate must be a positive number")
|
||||
elif cantitate > 999: # INT(3) limit
|
||||
warnings.append("Cantitate exceeds 999 (will be truncated)")
|
||||
except ValueError:
|
||||
errors.append("Cantitate must be a valid number")
|
||||
|
||||
# Validate numeric fields (optional but must be valid if provided)
|
||||
for field, max_val in [('nr_linie_com_client', 999), ('line_number', 999)]:
|
||||
value = row_data.get(field, '').strip()
|
||||
if value:
|
||||
try:
|
||||
num_val = int(value)
|
||||
if num_val < 0:
|
||||
warnings.append(f"{field} should be positive")
|
||||
elif num_val > max_val:
|
||||
warnings.append(f"{field} exceeds {max_val} (will be truncated)")
|
||||
except ValueError:
|
||||
errors.append(f"{field} must be a valid number")
|
||||
|
||||
# Validate string length limits
|
||||
field_limits = {
|
||||
'comanda_productie': 15,
|
||||
'cod_articol': 15,
|
||||
'descr_com_prod': 50,
|
||||
'com_achiz_client': 25,
|
||||
'customer_name': 50,
|
||||
'customer_article_number': 25,
|
||||
'open_for_order': 25
|
||||
}
|
||||
|
||||
for field, max_len in field_limits.items():
|
||||
value = row_data.get(field, '').strip()
|
||||
if value and len(value) > max_len:
|
||||
warnings.append(f"{field} exceeds {max_len} characters (will be truncated)")
|
||||
|
||||
return errors, warnings
|
||||
|
||||
def add_order_to_database(order_data):
|
||||
"""
|
||||
Add a single order to the order_for_labels table
|
||||
Returns (success: bool, message: str)
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Prepare data with proper types and limits
|
||||
insert_data = {
|
||||
'comanda_productie': order_data.get('comanda_productie', '').strip()[:15],
|
||||
'cod_articol': order_data.get('cod_articol', '').strip()[:15] or None,
|
||||
'descr_com_prod': order_data.get('descr_com_prod', '').strip()[:50],
|
||||
'cantitate': int(order_data.get('cantitate', 0)),
|
||||
'com_achiz_client': order_data.get('com_achiz_client', '').strip()[:25] or None,
|
||||
'nr_linie_com_client': int(order_data.get('nr_linie_com_client', 0)) if order_data.get('nr_linie_com_client', '').strip() else None,
|
||||
'customer_name': order_data.get('customer_name', '').strip()[:50] or None,
|
||||
'customer_article_number': order_data.get('customer_article_number', '').strip()[:25] or None,
|
||||
'open_for_order': order_data.get('open_for_order', '').strip()[:25] or None,
|
||||
'line_number': int(order_data.get('line_number', 0)) if order_data.get('line_number', '').strip() else None
|
||||
}
|
||||
|
||||
sql = """
|
||||
INSERT INTO order_for_labels
|
||||
(comanda_productie, cod_articol, descr_com_prod, cantitate,
|
||||
com_achiz_client, nr_linie_com_client, customer_name,
|
||||
customer_article_number, open_for_order, line_number)
|
||||
VALUES (%(comanda_productie)s, %(cod_articol)s, %(descr_com_prod)s, %(cantitate)s,
|
||||
%(com_achiz_client)s, %(nr_linie_com_client)s, %(customer_name)s,
|
||||
%(customer_article_number)s, %(open_for_order)s, %(line_number)s)
|
||||
"""
|
||||
|
||||
cursor.execute(sql, insert_data)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return True, f"Successfully added order {insert_data['comanda_productie']}"
|
||||
|
||||
except mariadb.Error as e:
|
||||
return False, f"Database error: {str(e)}"
|
||||
except ValueError as e:
|
||||
return False, f"Data validation error: {str(e)}"
|
||||
except Exception as e:
|
||||
return False, f"Unexpected error: {str(e)}"
|
||||
|
||||
def process_csv_file(file_path):
|
||||
"""
|
||||
Process uploaded CSV file and return parsed data with validation
|
||||
Returns: (orders_data: list, validation_errors: list, validation_warnings: list)
|
||||
"""
|
||||
orders_data = []
|
||||
all_errors = []
|
||||
all_warnings = []
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
# Try to detect the CSV dialect
|
||||
sample = f.read(1024)
|
||||
f.seek(0)
|
||||
|
||||
# Create CSV reader
|
||||
reader = csv.DictReader(f)
|
||||
|
||||
# Map possible column names (case-insensitive)
|
||||
column_mapping = {
|
||||
'comanda productie': 'comanda_productie',
|
||||
'cod articol': 'cod_articol',
|
||||
'descr. com. prod': 'descr_com_prod',
|
||||
'cantitate': 'cantitate',
|
||||
'com.achiz.client': 'com_achiz_client',
|
||||
'nr. linie com. client': 'nr_linie_com_client',
|
||||
'customer name': 'customer_name',
|
||||
'customer article number': 'customer_article_number',
|
||||
'open for order': 'open_for_order',
|
||||
'line': 'line_number'
|
||||
}
|
||||
|
||||
for row_num, row in enumerate(reader, start=2): # Start at 2 (row 1 is header)
|
||||
# Normalize column names and create order data
|
||||
normalized_row = {}
|
||||
for col_name, col_value in row.items():
|
||||
if col_name:
|
||||
col_key = col_name.lower().strip()
|
||||
mapped_key = column_mapping.get(col_key, col_key.replace(' ', '_').replace('.', '_'))
|
||||
normalized_row[mapped_key] = col_value.strip() if col_value else ''
|
||||
|
||||
# Validate the row
|
||||
errors, warnings = validate_order_row(normalized_row)
|
||||
|
||||
if errors:
|
||||
all_errors.extend([f"Row {row_num}: {error}" for error in errors])
|
||||
else:
|
||||
# Only add valid rows
|
||||
orders_data.append(normalized_row)
|
||||
|
||||
if warnings:
|
||||
all_warnings.extend([f"Row {row_num}: {warning}" for warning in warnings])
|
||||
|
||||
except UnicodeDecodeError:
|
||||
# Try different encodings
|
||||
try:
|
||||
with open(file_path, 'r', encoding='latin-1') as f:
|
||||
reader = csv.DictReader(f)
|
||||
# ... repeat the same processing logic
|
||||
except Exception as e:
|
||||
all_errors.append(f"File encoding error: {str(e)}")
|
||||
except Exception as e:
|
||||
all_errors.append(f"File processing error: {str(e)}")
|
||||
|
||||
return orders_data, all_errors, all_warnings
|
||||
|
||||
def upload_orders_handler():
|
||||
"""
|
||||
Main handler for the upload orders functionality
|
||||
Handles both CSV upload/preview and database insertion
|
||||
"""
|
||||
report = None
|
||||
orders_data = []
|
||||
validation_errors = []
|
||||
validation_warnings = []
|
||||
temp_dir = tempfile.gettempdir()
|
||||
|
||||
if request.method == 'POST':
|
||||
# Handle file upload
|
||||
file = request.files.get('csv_file')
|
||||
if file and file.filename.endswith(('.csv', '.CSV')):
|
||||
try:
|
||||
# Save uploaded file
|
||||
temp_path = os.path.join(temp_dir, file.filename)
|
||||
file.save(temp_path)
|
||||
|
||||
# Store file info in session
|
||||
session['csv_filename'] = file.filename
|
||||
session['orders_csv_filepath'] = temp_path
|
||||
|
||||
# Process the CSV file
|
||||
orders_data, validation_errors, validation_warnings = process_csv_file(temp_path)
|
||||
|
||||
# Store processed data in session
|
||||
session['orders_csv_data'] = orders_data
|
||||
session['orders_validation_errors'] = validation_errors
|
||||
session['orders_validation_warnings'] = validation_warnings
|
||||
|
||||
flash(f"📁 File '{file.filename}' uploaded and processed successfully!", "info")
|
||||
|
||||
if validation_errors:
|
||||
flash(f"⚠️ Found {len(validation_errors)} validation errors. Please fix them before importing.", "warning")
|
||||
|
||||
if validation_warnings:
|
||||
flash(f"ℹ️ Found {len(validation_warnings)} warnings. Data will be adjusted automatically.", "info")
|
||||
|
||||
except Exception as e:
|
||||
flash(f"❌ Error processing file: {str(e)}", "error")
|
||||
|
||||
# Handle database insertion
|
||||
elif request.form.get('save_to_database') and 'orders_csv_data' in session:
|
||||
orders_data = session['orders_csv_data']
|
||||
|
||||
if not orders_data:
|
||||
flash("❌ No valid data to save to database.", "error")
|
||||
else:
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
failed_orders = []
|
||||
|
||||
for order in orders_data:
|
||||
success, message = add_order_to_database(order)
|
||||
if success:
|
||||
success_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
failed_orders.append(f"{order.get('comanda_productie', 'Unknown')}: {message}")
|
||||
|
||||
# Create report
|
||||
report = f"✅ Successfully imported {success_count} orders."
|
||||
if failed_count > 0:
|
||||
report += f" ❌ {failed_count} orders failed to import."
|
||||
for failure in failed_orders[:5]: # Show first 5 failures
|
||||
report += f"<br>• {failure}"
|
||||
if len(failed_orders) > 5:
|
||||
report += f"<br>• ... and {len(failed_orders) - 5} more failures."
|
||||
|
||||
# Clear session data after successful import
|
||||
if success_count > 0:
|
||||
session.pop('orders_csv_data', None)
|
||||
session.pop('csv_filename', None)
|
||||
session.pop('orders_csv_filepath', None)
|
||||
session.pop('orders_validation_errors', None)
|
||||
session.pop('orders_validation_warnings', None)
|
||||
|
||||
flash(report, "success" if failed_count == 0 else "warning")
|
||||
|
||||
return redirect(url_for('main.upload_orders') + '#imported')
|
||||
|
||||
# Load data from session if available
|
||||
elif 'orders_csv_data' in session:
|
||||
orders_data = session['orders_csv_data']
|
||||
validation_errors = session.get('orders_validation_errors', [])
|
||||
validation_warnings = session.get('orders_validation_warnings', [])
|
||||
|
||||
return render_template('upload_orders.html',
|
||||
orders=orders_data,
|
||||
validation_errors=validation_errors,
|
||||
validation_warnings=validation_warnings,
|
||||
report=report)
|
||||
|
||||
def get_orders_from_database(limit=100):
|
||||
"""
|
||||
Retrieve orders from the database for display
|
||||
Returns list of order dictionaries
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
|
||||
com_achiz_client, nr_linie_com_client, customer_name,
|
||||
customer_article_number, open_for_order, line_number,
|
||||
printed_labels, created_at, updated_at
|
||||
FROM order_for_labels
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s
|
||||
""", (limit,))
|
||||
|
||||
orders = []
|
||||
for row in cursor.fetchall():
|
||||
orders.append({
|
||||
'id': row[0],
|
||||
'comanda_productie': row[1],
|
||||
'cod_articol': row[2],
|
||||
'descr_com_prod': row[3],
|
||||
'cantitate': row[4],
|
||||
'com_achiz_client': row[5],
|
||||
'nr_linie_com_client': row[6],
|
||||
'customer_name': row[7],
|
||||
'customer_article_number': row[8],
|
||||
'open_for_order': row[9],
|
||||
'line_number': row[10],
|
||||
'printed_labels': row[11],
|
||||
'created_at': row[12],
|
||||
'updated_at': row[13]
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return orders
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving orders: {e}")
|
||||
return []
|
||||
@@ -1002,6 +1002,28 @@ def generate_pdf():
|
||||
|
||||
return jsonify({'message': 'PDF generated successfully!', 'pdf_path': f'/static/label_templates/label_template.pdf'})
|
||||
|
||||
# Order Labels Upload Module Routes
|
||||
@bp.route('/upload_orders', methods=['GET', 'POST'])
|
||||
def upload_orders():
|
||||
"""Route for uploading orders CSV files for label generation"""
|
||||
if 'role' not in session or session['role'] not in ['superadmin', 'warehouse', 'warehouse_manager']:
|
||||
flash('Access denied: Warehouse management permissions required.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
from app.order_labels import upload_orders_handler
|
||||
return upload_orders_handler()
|
||||
|
||||
@bp.route('/view_orders')
|
||||
def view_orders():
|
||||
"""Route for viewing uploaded orders"""
|
||||
if 'role' not in session or session['role'] not in ['superadmin', 'warehouse', 'warehouse_manager', 'warehouse_worker']:
|
||||
flash('Access denied: Warehouse access required.')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
from app.order_labels import get_orders_from_database
|
||||
orders = get_orders_from_database(200) # Get last 200 orders
|
||||
return render_template('view_orders.html', orders=orders)
|
||||
|
||||
@warehouse_bp.route('/create_locations', methods=['GET', 'POST'])
|
||||
def create_locations():
|
||||
from app.warehouse import create_locations_handler
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
<!-- Row of evenly distributed cards -->
|
||||
<div class="dashboard-container">
|
||||
<!-- Card 1: Upload Data -->
|
||||
<!-- Card 1: View Orders -->
|
||||
<div class="dashboard-card">
|
||||
<h3>Upload Data</h3>
|
||||
<p>Upload data into the database for label management.</p>
|
||||
<a href="{{ url_for('main.upload_data') }}" class="btn">Go to Upload Data</a>
|
||||
<h3>View Orders</h3>
|
||||
<p>View uploaded orders and manage label data for printing.</p>
|
||||
<a href="{{ url_for('main.view_orders') }}" class="btn">View Orders</a>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Launch Print Module -->
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Upload Data{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="upload-data-container">
|
||||
<h1>Upload Data</h1>
|
||||
<p>This page will allow users to upload data into the database.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
244
py_app/app/templates/upload_orders.html
Normal file
244
py_app/app/templates/upload_orders.html
Normal file
@@ -0,0 +1,244 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Upload Order Data for Labels{% endblock %}
|
||||
{% block content %}
|
||||
<div class="scan-container">
|
||||
<!-- Upload Orders Card (first, fixed position) -->
|
||||
<div class="card scan-form-card" style="margin-bottom: 24px;">
|
||||
<h3>Upload Order Data for Labels</h3>
|
||||
<form method="POST" enctype="multipart/form-data" class="form-centered" id="csv-upload-form">
|
||||
<label for="csv_file">Choose CSV file:</label>
|
||||
{% if not orders %}
|
||||
<input type="file" name="csv_file" accept=".csv" required><br>
|
||||
<button type="submit" class="btn">Upload & Preview</button>
|
||||
{% else %}
|
||||
<label style="font-weight: bold;">Selected file: {{ session['csv_filename'] if session['csv_filename'] else 'Unknown' }}</label><br>
|
||||
<button type="button" class="btn" onclick="showPopupAndSubmit()">Save to Database</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<!-- Popup Modal -->
|
||||
<div id="popup-modal" class="popup" style="display:none; position:fixed; top:0; left:0; width:100vw; height:100vh; background:var(--app-overlay-bg, rgba(30,41,59,0.85)); z-index:9999; align-items:center; justify-content:center;">
|
||||
<div class="popup-content" style="margin:auto; padding:32px; border-radius:8px; box-shadow:0 2px 8px #333; min-width:320px; max-width:400px; text-align:center;">
|
||||
<h3 style="color:var(--app-label-text);">Saving orders to database...</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showPopupAndSubmit() {
|
||||
document.getElementById('popup-modal').style.display = 'flex';
|
||||
// Submit the form after showing popup
|
||||
setTimeout(function() {
|
||||
var form = document.getElementById('csv-upload-form');
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'save_to_database';
|
||||
input.value = '1';
|
||||
form.appendChild(input);
|
||||
form.submit();
|
||||
}, 500);
|
||||
}
|
||||
window.onload = function() {
|
||||
if (window.location.hash === '#saved') {
|
||||
document.getElementById('popup-modal').style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- Preview Table Card (expandable height, scrollable) -->
|
||||
<div class="card scan-table-card" style="margin-bottom: 24px; max-height: 480px; overflow-y: auto;">
|
||||
<h3>Preview Table</h3>
|
||||
<table class="scan-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Comanda Productie</th>
|
||||
<th>Cod Articol</th>
|
||||
<th>Descr. Com. Prod</th>
|
||||
<th>Cantitate</th>
|
||||
<th>Com.Achiz.Client</th>
|
||||
<th>Nr. Linie com. Client</th>
|
||||
<th>Customer Name</th>
|
||||
<th>Customer Article Number</th>
|
||||
<th>Open for order</th>
|
||||
<th>Line</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if orders %}
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order.get('comanda_productie', '') }}</td>
|
||||
<td>{{ order.get('cod_articol', '') }}</td>
|
||||
<td>{{ order.get('descr_com_prod', '') }}</td>
|
||||
<td>{{ order.get('cantitate', '') }}</td>
|
||||
<td>{{ order.get('com_achiz_client', '') }}</td>
|
||||
<td>{{ order.get('nr_linie_com_client', '') }}</td>
|
||||
<td>{{ order.get('customer_name', '') }}</td>
|
||||
<td>{{ order.get('customer_article_number', '') }}</td>
|
||||
<td>{{ order.get('open_for_order', '') }}</td>
|
||||
<td>{{ order.get('line_number', '') }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr><td colspan="10" style="text-align:center;">No CSV file uploaded yet.</td></tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if validation_errors or validation_warnings %}
|
||||
<div class="card" style="margin-bottom: 24px;">
|
||||
<h4>Validation Results</h4>
|
||||
|
||||
{% if validation_errors %}
|
||||
<div style="color: #dc3545; margin-bottom: 16px;">
|
||||
<strong>Errors found:</strong>
|
||||
<ul>
|
||||
{% for error in validation_errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if validation_warnings %}
|
||||
<div style="color: #ffc107;">
|
||||
<strong>Warnings:</strong>
|
||||
<ul>
|
||||
{% for warning in validation_warnings %}
|
||||
<li>{{ warning }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if report %}
|
||||
<div class="card" style="margin-bottom: 24px;">
|
||||
<h4>Import Report</h4>
|
||||
<p>{{ report }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Optimize table for better content fitting */
|
||||
.scan-table {
|
||||
font-size: 11px !important;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.scan-table th,
|
||||
.scan-table td {
|
||||
padding: 6px 4px !important;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.scan-table th {
|
||||
font-size: 10px !important;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* Specific column width optimizations */
|
||||
.scan-table th:nth-child(1), /* Comanda Productie */
|
||||
.scan-table td:nth-child(1) {
|
||||
max-width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(2), /* Cod Articol */
|
||||
.scan-table td:nth-child(2) {
|
||||
max-width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(3), /* Descr. Com. Prod */
|
||||
.scan-table td:nth-child(3) {
|
||||
max-width: 120px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(4), /* Cantitate */
|
||||
.scan-table td:nth-child(4) {
|
||||
max-width: 60px;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(5), /* Com.Achiz.Client */
|
||||
.scan-table td:nth-child(5) {
|
||||
max-width: 90px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(6), /* Nr. Linie com. Client */
|
||||
.scan-table td:nth-child(6) {
|
||||
max-width: 60px;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(7), /* Customer Name */
|
||||
.scan-table td:nth-child(7) {
|
||||
max-width: 100px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(8), /* Customer Article Number */
|
||||
.scan-table td:nth-child(8) {
|
||||
max-width: 80px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(9), /* Open for order */
|
||||
.scan-table td:nth-child(9) {
|
||||
max-width: 50px;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scan-table th:nth-child(10), /* Line */
|
||||
.scan-table td:nth-child(10) {
|
||||
max-width: 40px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Ensure table fits in container */
|
||||
.scan-table-card {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.scan-table {
|
||||
min-width: 800px;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Add hover effect for better readability */
|
||||
.scan-table tbody tr:hover td {
|
||||
background-color: #f8f9fa !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Tooltip on hover for truncated text */
|
||||
.scan-table td {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.scan-table td:hover {
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
background-color: #fff3cd !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
266
py_app/app/templates/view_orders.html
Normal file
266
py_app/app/templates/view_orders.html
Normal file
@@ -0,0 +1,266 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}View Uploaded Orders{% endblock %}
|
||||
{% block content %}
|
||||
<div class="scan-container">
|
||||
<!-- Orders Actions Card (first, narrower) -->
|
||||
<div class="card report-form-card">
|
||||
<h3>List Uploaded Orders</h3>
|
||||
|
||||
<!-- Orders Actions -->
|
||||
<div class="reports-grid">
|
||||
<div class="form-centered">
|
||||
<label class="report-description">View all uploaded orders in the current day for labels generation</label>
|
||||
<button class="btn report-btn" onclick="window.location.reload()">Refresh Orders List</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">View all uploaded orders for labels generation</label>
|
||||
<button class="btn report-btn" onclick="window.location.reload()">Fetch Orders List</button>
|
||||
</div>
|
||||
<div class="form-centered">
|
||||
<label class="report-description">Upload new CSV file with order data</label>
|
||||
<button class="btn report-btn" onclick="location.href='/upload_orders'">Upload New Orders</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Separator -->
|
||||
<div class="report-separator"></div>
|
||||
|
||||
<!-- Export Section -->
|
||||
<div class="export-section">
|
||||
<div class="form-centered last-buttons">
|
||||
<label class="export-description">Export orders as:</label>
|
||||
<div class="button-row">
|
||||
<button class="btn export-btn" onclick="window.print()">Print Orders</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Orders Table Card (second, wider) -->
|
||||
<div class="card report-table-card">
|
||||
{% if orders %}
|
||||
<h3>Uploaded Orders ({{ orders|length }} total)</h3>
|
||||
{% else %}
|
||||
<h3>No orders uploaded yet</h3>
|
||||
{% endif %}
|
||||
|
||||
<div class="report-table-container">
|
||||
{% if orders %}
|
||||
<table class="scan-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Comanda Productie</th>
|
||||
<th>Cod Articol</th>
|
||||
<th>Descr. Com. Prod</th>
|
||||
<th>Cantitate</th>
|
||||
<th>Com.Achiz.Client</th>
|
||||
<th>Nr. Linie</th>
|
||||
<th>Customer Name</th>
|
||||
<th>Customer Art. Nr.</th>
|
||||
<th>Open Order</th>
|
||||
<th>Line</th>
|
||||
<th>Printed</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for order in orders %}
|
||||
<tr>
|
||||
<td>{{ order.id }}</td>
|
||||
<td><strong>{{ order.comanda_productie }}</strong></td>
|
||||
<td>{{ order.cod_articol or '-' }}</td>
|
||||
<td>{{ order.descr_com_prod }}</td>
|
||||
<td style="text-align: right; font-weight: 600;">{{ order.cantitate }}</td>
|
||||
<td>{{ order.com_achiz_client or '-' }}</td>
|
||||
<td style="text-align: right;">{{ order.nr_linie_com_client or '-' }}</td>
|
||||
<td>{{ order.customer_name or '-' }}</td>
|
||||
<td>{{ order.customer_article_number or '-' }}</td>
|
||||
<td>{{ order.open_for_order or '-' }}</td>
|
||||
<td style="text-align: right;">{{ order.line_number or '-' }}</td>
|
||||
<td style="text-align: center;">
|
||||
{% if order.printed_labels == 1 %}
|
||||
<span style="color: #28a745; font-weight: bold;">✓ Yes</span>
|
||||
{% else %}
|
||||
<span style="color: #dc3545;">✗ No</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size: 11px; color: #6c757d;">
|
||||
{% if order.created_at %}
|
||||
{{ order.created_at.strftime('%Y-%m-%d %H:%M') }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div style="text-align: center; padding: 40px; color: #6c757d;">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">📦</div>
|
||||
<h4>No Orders Found</h4>
|
||||
<p>Upload your first CSV file to see orders here.</p>
|
||||
<button class="btn report-btn" onclick="location.href='/upload_orders'" style="margin-top: 20px;">
|
||||
Upload Orders
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* View Orders Table Optimization - Higher specificity to override base styles */
|
||||
.report-table-card .scan-table {
|
||||
font-size: 11px !important;
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th {
|
||||
font-size: 10px !important;
|
||||
padding: 8px 6px !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table td {
|
||||
padding: 6px 6px !important;
|
||||
font-size: 11px !important;
|
||||
line-height: 1.2 !important;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
/* Specific column optimizations for better content fitting */
|
||||
.report-table-card .scan-table th:nth-child(1), /* ID */
|
||||
.report-table-card .scan-table td:nth-child(1) {
|
||||
max-width: 50px !important;
|
||||
min-width: 50px !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(2), /* Comanda Productie */
|
||||
.report-table-card .scan-table td:nth-child(2) {
|
||||
max-width: 90px !important;
|
||||
min-width: 90px !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(3), /* Cod Articol */
|
||||
.report-table-card .scan-table td:nth-child(3) {
|
||||
max-width: 80px !important;
|
||||
min-width: 80px !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(4), /* Descr. Com. Prod */
|
||||
.report-table-card .scan-table td:nth-child(4) {
|
||||
max-width: 140px !important;
|
||||
min-width: 140px !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(5), /* Cantitate */
|
||||
.report-table-card .scan-table td:nth-child(5) {
|
||||
max-width: 70px !important;
|
||||
min-width: 70px !important;
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(6), /* Com.Achiz.Client */
|
||||
.report-table-card .scan-table td:nth-child(6) {
|
||||
max-width: 100px !important;
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(7), /* Nr. Linie */
|
||||
.report-table-card .scan-table td:nth-child(7) {
|
||||
max-width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(8), /* Customer Name */
|
||||
.report-table-card .scan-table td:nth-child(8) {
|
||||
max-width: 120px !important;
|
||||
min-width: 120px !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(9), /* Customer Art. Nr. */
|
||||
.report-table-card .scan-table td:nth-child(9) {
|
||||
max-width: 90px !important;
|
||||
min-width: 90px !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(10), /* Open Order */
|
||||
.report-table-card .scan-table td:nth-child(10) {
|
||||
max-width: 70px !important;
|
||||
min-width: 70px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(11), /* Line */
|
||||
.report-table-card .scan-table td:nth-child(11) {
|
||||
max-width: 50px !important;
|
||||
min-width: 50px !important;
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(12), /* Printed */
|
||||
.report-table-card .scan-table td:nth-child(12) {
|
||||
max-width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table th:nth-child(13), /* Created */
|
||||
.report-table-card .scan-table td:nth-child(13) {
|
||||
max-width: 110px !important;
|
||||
min-width: 110px !important;
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
/* Hover effects for better readability */
|
||||
.report-table-card .scan-table tbody tr:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table tbody tr:hover td {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
/* Table container optimization */
|
||||
.report-table-card .report-table-container {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
|
||||
.report-table-card .scan-table {
|
||||
min-width: 1060px !important;
|
||||
width: 100% !important;
|
||||
table-layout: fixed !important;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.report-form-card {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.scan-table {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
|
||||
.scan-table th,
|
||||
.scan-table td {
|
||||
padding: 3px 2px !important;
|
||||
font-size: 9px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user