V1.0.0.2 - Refactor: Split DB and Import logic, fixed CSV upload columns
This commit is contained in:
BIN
__pycache__/db.cpython-313.pyc
Normal file
BIN
__pycache__/db.cpython-313.pyc
Normal file
Binary file not shown.
149
app.py
149
app.py
@@ -11,8 +11,13 @@ import csv
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from io import StringIO
|
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 = Flask(__name__)
|
||||||
|
|
||||||
|
app.register_blueprint(data_imports_bp)
|
||||||
|
|
||||||
# V1.0: Use environment variable for production, fallback to demo key for development
|
# 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.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')
|
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)
|
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 ====================
|
# ==================== AUTHENTICATION DECORATORS ====================
|
||||||
|
|
||||||
@@ -383,123 +361,6 @@ def get_status_details(session_id, status):
|
|||||||
return jsonify({'success': False, 'message': f'Error: {str(e)}'})
|
return jsonify({'success': False, 'message': f'Error: {str(e)}'})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/session/<int:session_id>/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) ====================
|
# ==================== ROUTES: COUNTING (STAFF) ====================
|
||||||
|
|||||||
BIN
blueprints/__pycache__/data_imports.cpython-313.pyc
Normal file
BIN
blueprints/__pycache__/data_imports.cpython-313.pyc
Normal file
Binary file not shown.
155
blueprints/data_imports.py
Normal file
155
blueprints/data_imports.py
Normal file
@@ -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/<int:session_id>', 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/<int:session_id>/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))
|
||||||
Binary file not shown.
26
db.py
Normal file
26
db.py
Normal file
@@ -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
|
||||||
@@ -30,8 +30,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if not count_session.master_baseline_timestamp %}
|
{% if not count_session.master_baseline_timestamp %}
|
||||||
<form method="POST" action="{{ url_for('upload_baseline', session_id=count_session.session_id) }}" enctype="multipart/form-data" class="upload-form">
|
<form method="POST" action="{{ url_for('data_imports.upload_master', session_id=count_session.session_id) }}" enctype="multipart/form-data" class="upload-form">
|
||||||
<input type="hidden" name="baseline_type" value="master">
|
|
||||||
<input type="file" name="csv_file" accept=".csv" required class="file-input">
|
<input type="file" name="csv_file" accept=".csv" required class="file-input">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Upload MASTER</button>
|
<button type="submit" class="btn btn-primary btn-sm">Upload MASTER</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if count_session.master_baseline_timestamp %}
|
{% if count_session.master_baseline_timestamp %}
|
||||||
<form method="POST" action="{{ url_for('upload_baseline', session_id=count_session.session_id) }}" enctype="multipart/form-data" class="upload-form">
|
<form method="POST" action="{{ url_for('data_imports.upload_current', session_id=count_session.session_id) }}" enctype="multipart/form-data" class="upload-form">
|
||||||
<input type="hidden" name="baseline_type" value="current">
|
<input type="hidden" name="baseline_type" value="current">
|
||||||
<input type="file" name="csv_file" accept=".csv" required class="file-input">
|
<input type="file" name="csv_file" accept=".csv" required class="file-input">
|
||||||
<button type="submit" class="btn btn-secondary btn-sm">
|
<button type="submit" class="btn btn-secondary btn-sm">
|
||||||
|
|||||||
Reference in New Issue
Block a user