diff --git a/__pycache__/db.cpython-313.pyc b/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000..c9d8954 Binary files /dev/null and b/__pycache__/db.cpython-313.pyc differ diff --git a/app.py b/app.py index e0550cb..c2c8bea 100644 --- a/app.py +++ b/app.py @@ -11,8 +11,13 @@ import csv import os from datetime import datetime, timedelta from io import StringIO +from db import query_db, execute_db, get_db +from blueprints.data_imports import data_imports_bp app = Flask(__name__) + +app.register_blueprint(data_imports_bp) + # V1.0: Use environment variable for production, fallback to demo key for development app.secret_key = os.environ.get('SCANLOOK_SECRET_KEY', 'scanlook-demo-key-replace-for-production') app.config['DATABASE'] = os.path.join(os.path.dirname(__file__), 'database', 'scanlook.db') @@ -21,33 +26,6 @@ app.config['DATABASE'] = os.path.join(os.path.dirname(__file__), 'database', 'sc app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1) -# ==================== DATABASE HELPERS ==================== - -def get_db(): - """Get database connection""" - conn = sqlite3.connect(app.config['DATABASE']) - conn.row_factory = sqlite3.Row - return conn - - -def query_db(query, args=(), one=False): - """Query database helper""" - conn = get_db() - cursor = conn.execute(query, args) - rv = cursor.fetchall() - conn.close() - return (rv[0] if rv else None) if one else rv - - -def execute_db(query, args=()): - """Execute database insert/update/delete""" - conn = get_db() - cursor = conn.execute(query, args) - conn.commit() - last_id = cursor.lastrowid - conn.close() - return last_id - # ==================== AUTHENTICATION DECORATORS ==================== @@ -383,123 +361,6 @@ def get_status_details(session_id, status): return jsonify({'success': False, 'message': f'Error: {str(e)}'}) -@app.route('/session//upload_baseline', methods=['POST']) -@role_required('owner', 'admin') -def upload_baseline(session_id): - """Upload MASTER or CURRENT baseline CSV""" - baseline_type = request.form.get('baseline_type', 'master') - - if 'csv_file' not in request.files: - flash('No file uploaded', 'danger') - return redirect(url_for('session_detail', session_id=session_id)) - - file = request.files['csv_file'] - - if file.filename == '': - flash('No file selected', 'danger') - return redirect(url_for('session_detail', session_id=session_id)) - - try: - # Read CSV - stream = StringIO(file.stream.read().decode("UTF8"), newline=None) - csv_reader = csv.DictReader(stream) - - # Validate columns - required_columns = ['Item', 'Description', 'Lot Number', 'Location', 'Bin Number', 'On Hand'] - if not all(col in csv_reader.fieldnames for col in required_columns): - flash(f'CSV missing required columns. Need: {", ".join(required_columns)}', 'danger') - return redirect(url_for('session_detail', session_id=session_id)) - - conn = get_db() - cursor = conn.cursor() - count = 0 - - if baseline_type == 'master': - # Upload MASTER baseline - consolidate duplicates by location - lot_location_data = {} - - for row in csv_reader: - lot_num = row['Lot Number'].strip() - bin_num = row['Bin Number'].strip() - key = (lot_num, bin_num) - - if key in lot_location_data: - # Duplicate in same location - add to existing - lot_location_data[key]['quantity'] += float(row['On Hand']) - else: - # New lot/location combination - lot_location_data[key] = { - 'item': row['Item'].strip(), - 'description': row['Description'].strip(), - 'location': row['Location'].strip(), - 'bin': bin_num, - 'quantity': float(row['On Hand']) - } - - # Insert consolidated data - for (lot_num, bin_num), data in lot_location_data.items(): - cursor.execute(''' - INSERT INTO BaselineInventory_Master - (session_id, lot_number, item, description, system_location, system_bin, system_quantity) - VALUES (?, ?, ?, ?, ?, ?, ?) - ''', [ - session_id, - lot_num, - data['item'], - data['description'], - data['location'], - data['bin'], - data['quantity'] - ]) - count += 1 - - # Update session - cursor.execute(''' - UPDATE CountSessions - SET master_baseline_timestamp = CURRENT_TIMESTAMP - WHERE session_id = ? - ''', [session_id]) - - flash(f'✅ MASTER baseline uploaded: {count} records', 'success') - - else: - # Upload CURRENT baseline (GLOBAL - not session-specific) - # Simple: Delete all old data, insert new data - - # Delete all existing CURRENT data - cursor.execute('DELETE FROM BaselineInventory_Current') - - # Insert new CURRENT baseline - for row in csv_reader: - cursor.execute(''' - INSERT INTO BaselineInventory_Current - (lot_number, item, description, system_location, system_bin, system_quantity) - VALUES (?, ?, ?, ?, ?, ?) - ''', [ - row['Lot Number'].strip(), - row['Item'].strip(), - row['Description'].strip(), - row['Location'].strip(), - row['Bin Number'].strip(), - float(row['On Hand']) - ]) - count += 1 - - # Update ALL sessions with current timestamp - cursor.execute(''' - UPDATE CountSessions - SET current_baseline_timestamp = CURRENT_TIMESTAMP - ''') - - flash(f'✅ CURRENT baseline uploaded: {count} records (global)', 'success') - - conn.commit() - conn.close() - - except Exception as e: - flash(f'Error uploading CSV: {str(e)}', 'danger') - - return redirect(url_for('session_detail', session_id=session_id)) # ==================== ROUTES: COUNTING (STAFF) ==================== diff --git a/blueprints/__pycache__/data_imports.cpython-313.pyc b/blueprints/__pycache__/data_imports.cpython-313.pyc new file mode 100644 index 0000000..944775e Binary files /dev/null and b/blueprints/__pycache__/data_imports.cpython-313.pyc differ diff --git a/blueprints/data_imports.py b/blueprints/data_imports.py new file mode 100644 index 0000000..89cca65 --- /dev/null +++ b/blueprints/data_imports.py @@ -0,0 +1,155 @@ +import csv +import io +from flask import Blueprint, request, flash, redirect, url_for, session +from db import execute_db, get_db + +data_imports_bp = Blueprint('data_imports', __name__) + +def login_required_check(): + if 'user_id' not in session: + return False + return True + +# --- ROUTE 1: Upload CURRENT Inventory (Global) --- +@data_imports_bp.route('/upload_current/', methods=['POST']) +def upload_current(session_id): + if not login_required_check(): return redirect(url_for('login')) + + if 'csv_file' not in request.files: + flash('No file part', 'danger') + return redirect(url_for('session_detail', session_id=session_id)) + + file = request.files['csv_file'] + if file.filename == '': + flash('No selected file', 'danger') + return redirect(url_for('session_detail', session_id=session_id)) + + if file: + conn = get_db() + cursor = conn.cursor() + try: + stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None) + csv_input = csv.DictReader(stream) + + # 1. Reset Table + cursor.execute('DROP TABLE IF EXISTS BaselineInventory_Current') + cursor.execute(''' + CREATE TABLE BaselineInventory_Current ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + item TEXT, + lot_number TEXT, + system_bin TEXT, + system_quantity REAL, + uom TEXT + ) + ''') + + # 2. BULK INSERT with Correct Headers + rows_to_insert = [] + for row in csv_input: + # Clean up keys (remove hidden characters/spaces) + row = {k.strip(): v for k, v in row.items()} + + rows_to_insert.append(( + row.get('Item', ''), + row.get('Lot Number', ''), # FIX: Changed from 'Lot' + row.get('Bin Number', ''), # FIX: Changed from 'Bin' + row.get('On Hand', 0), # FIX: Changed from 'Qty' + row.get('UOM', 'LBS') + )) + + cursor.executemany(''' + INSERT INTO BaselineInventory_Current + (item, lot_number, system_bin, system_quantity, uom) + VALUES (?, ?, ?, ?, ?) + ''', rows_to_insert) + + # 3. Update timestamp + cursor.execute('UPDATE CountSessions SET current_baseline_timestamp = CURRENT_TIMESTAMP') + + conn.commit() + flash(f'Successfully uploaded {len(rows_to_insert)} records.', 'success') + + except Exception as e: + conn.rollback() + flash(f'Error uploading CSV: {str(e)}', 'danger') + finally: + conn.close() + + return redirect(url_for('session_detail', session_id=session_id)) + +# --- ROUTE 2: Upload MASTER Baseline (Session Specific) --- +@data_imports_bp.route('/session//upload_master', methods=['POST']) +def upload_master(session_id): + if not login_required_check(): return redirect(url_for('login')) + + if 'csv_file' not in request.files: + flash('No file uploaded', 'danger') + return redirect(url_for('session_detail', session_id=session_id)) + + file = request.files['csv_file'] + if file.filename == '': + flash('No file selected', 'danger') + return redirect(url_for('session_detail', session_id=session_id)) + + conn = get_db() + cursor = conn.cursor() + try: + stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None) + csv_reader = csv.DictReader(stream) + + lot_location_data = {} + + # Consolidate duplicates in memory + for row in csv_reader: + # Clean keys here too just in case + row = {k.strip(): v for k, v in row.items()} + + lot_num = row.get('Lot Number', '').strip() + bin_num = row.get('Bin Number', '').strip() + key = (lot_num, bin_num) + + # Use get() to handle potential missing keys gracefully + qty = float(row.get('On Hand', 0)) + + if key in lot_location_data: + lot_location_data[key]['quantity'] += qty + else: + lot_location_data[key] = { + 'item': row.get('Item', '').strip(), + 'description': row.get('Description', '').strip(), + 'location': row.get('Location', '').strip(), + 'bin': bin_num, + 'quantity': qty + } + + # BULK INSERT + rows_to_insert = [] + for (lot_num, bin_num), data in lot_location_data.items(): + rows_to_insert.append(( + session_id, + lot_num, + data['item'], + data['description'], + data['location'], + data['bin'], + data['quantity'] + )) + + cursor.executemany(''' + INSERT INTO BaselineInventory_Master + (session_id, lot_number, item, description, system_location, system_bin, system_quantity) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', rows_to_insert) + + cursor.execute('UPDATE CountSessions SET master_baseline_timestamp = CURRENT_TIMESTAMP WHERE session_id = ?', [session_id]) + conn.commit() + flash(f'✅ MASTER baseline uploaded: {len(rows_to_insert)} records', 'success') + + except Exception as e: + conn.rollback() + flash(f'Error uploading Master CSV: {str(e)}', 'danger') + finally: + conn.close() + + return redirect(url_for('session_detail', session_id=session_id)) \ No newline at end of file diff --git a/database/scanlook.db b/database/scanlook.db index 17f041c..5795be9 100644 Binary files a/database/scanlook.db and b/database/scanlook.db differ diff --git a/db.py b/db.py new file mode 100644 index 0000000..bfbbf3b --- /dev/null +++ b/db.py @@ -0,0 +1,26 @@ +import sqlite3 +from flask import current_app, g + +def get_db(): + """Get database connection""" + # Use current_app.config to access settings from any module + conn = sqlite3.connect(current_app.config['DATABASE']) + conn.row_factory = sqlite3.Row + return conn + +def query_db(query, args=(), one=False): + """Query database helper""" + conn = get_db() + cursor = conn.execute(query, args) + rv = cursor.fetchall() + conn.close() + return (rv[0] if rv else None) if one else rv + +def execute_db(query, args=()): + """Execute database insert/update/delete""" + conn = get_db() + cursor = conn.execute(query, args) + conn.commit() + last_id = cursor.lastrowid + conn.close() + return last_id \ No newline at end of file diff --git a/templates/session_detail.html b/templates/session_detail.html index f006fce..9cf43b4 100644 --- a/templates/session_detail.html +++ b/templates/session_detail.html @@ -30,8 +30,7 @@ {% endif %} {% if not count_session.master_baseline_timestamp %} -
- +
@@ -49,7 +48,7 @@ {% endif %} {% if count_session.master_baseline_timestamp %} -
+