Files
ScanLook/database/init_db.py

378 lines
14 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ScanLook Database Initialization
Creates all tables and indexes for the inventory management system
UPDATED: Reflects post-migration schema (CURRENT baseline is now global)
"""
import sqlite3
import os
from datetime import datetime
from werkzeug.security import generate_password_hash
DB_PATH = os.path.join(os.path.dirname(__file__), 'scanlook.db')
def init_database():
"""Initialize the database with all tables and indexes"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Users Table
cursor.execute('''
CREATE TABLE IF NOT EXISTS Users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
full_name TEXT NOT NULL,
email TEXT,
role TEXT NOT NULL CHECK(role IN ('owner', 'admin', 'staff')),
is_active INTEGER DEFAULT 1,
branch TEXT DEFAULT 'Main',
created_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
''')
# CountSessions Table
# NOTE: current_baseline_version removed - CURRENT is now global
cursor.execute('''
CREATE TABLE IF NOT EXISTS CountSessions (
session_id INTEGER PRIMARY KEY AUTOINCREMENT,
session_name TEXT NOT NULL,
session_type TEXT NOT NULL CHECK(session_type IN ('cycle_count', 'full_physical')),
created_by INTEGER NOT NULL,
created_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
master_baseline_timestamp DATETIME,
current_baseline_timestamp DATETIME,
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'completed', 'archived')),
branch TEXT DEFAULT 'Main',
FOREIGN KEY (created_by) REFERENCES Users(user_id)
)
''')
# BaselineInventory_Master Table (Session-specific, immutable)
cursor.execute('''
CREATE TABLE IF NOT EXISTS BaselineInventory_Master (
baseline_id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
lot_number TEXT NOT NULL,
item TEXT NOT NULL,
description TEXT,
system_location TEXT NOT NULL,
system_bin TEXT NOT NULL,
system_quantity REAL NOT NULL,
uploaded_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (session_id) REFERENCES CountSessions(session_id)
)
''')
# BaselineInventory_Current Table (GLOBAL - shared across all sessions)
# MIGRATION CHANGE: No session_id, no baseline_version, no is_deleted
# This table is replaced entirely on each upload
cursor.execute('''
CREATE TABLE IF NOT EXISTS BaselineInventory_Current (
current_id INTEGER PRIMARY KEY AUTOINCREMENT,
lot_number TEXT NOT NULL,
item TEXT NOT NULL,
description TEXT,
system_location TEXT,
system_bin TEXT NOT NULL,
system_quantity REAL NOT NULL,
upload_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(lot_number, system_bin)
)
''')
# LocationCounts Table
cursor.execute('''
CREATE TABLE IF NOT EXISTS LocationCounts (
location_count_id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
location_name TEXT NOT NULL,
counted_by INTEGER NOT NULL,
start_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
end_timestamp DATETIME,
status TEXT DEFAULT 'not_started' CHECK(status IN ('not_started', 'in_progress', 'completed')),
expected_lots_master INTEGER DEFAULT 0,
lots_found INTEGER DEFAULT 0,
lots_missing INTEGER DEFAULT 0,
FOREIGN KEY (session_id) REFERENCES CountSessions(session_id),
FOREIGN KEY (counted_by) REFERENCES Users(user_id)
)
''')
# ScanEntries Table
# MIGRATION CHANGE: Removed current_* columns - now fetched via JOIN
cursor.execute('''
CREATE TABLE IF NOT EXISTS ScanEntries (
entry_id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
location_count_id INTEGER NOT NULL,
lot_number TEXT NOT NULL,
item TEXT,
description TEXT,
scanned_location TEXT NOT NULL,
actual_weight REAL NOT NULL,
scanned_by INTEGER NOT NULL,
scan_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
-- MASTER baseline comparison (immutable, set at scan time)
master_status TEXT CHECK(master_status IN ('match', 'wrong_location', 'ghost_lot', 'missing')),
master_expected_location TEXT,
master_expected_weight REAL,
master_variance_lbs REAL,
master_variance_pct REAL,
-- Duplicate detection
duplicate_status TEXT DEFAULT '00' CHECK(duplicate_status IN ('00', '01', '03', '04')),
duplicate_info TEXT,
-- CURRENT baseline comparison removed - now via JOIN in queries
-- Removed: current_status, current_system_location, current_system_weight,
-- current_variance_lbs, current_variance_pct, current_baseline_version
-- Metadata
comment TEXT,
is_deleted INTEGER DEFAULT 0,
deleted_by INTEGER,
deleted_timestamp DATETIME,
modified_timestamp DATETIME,
FOREIGN KEY (session_id) REFERENCES CountSessions(session_id),
FOREIGN KEY (location_count_id) REFERENCES LocationCounts(location_count_id),
FOREIGN KEY (scanned_by) REFERENCES Users(user_id),
FOREIGN KEY (deleted_by) REFERENCES Users(user_id)
)
''')
# MissingLots Table
cursor.execute('''
CREATE TABLE IF NOT EXISTS MissingLots (
missing_id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
location_count_id INTEGER,
lot_number TEXT NOT NULL,
item TEXT,
master_expected_location TEXT NOT NULL,
master_expected_quantity REAL NOT NULL,
current_system_location TEXT,
current_system_quantity REAL,
marked_by INTEGER NOT NULL,
marked_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
found_later TEXT DEFAULT 'N' CHECK(found_later IN ('Y', 'N')),
found_location TEXT,
FOREIGN KEY (session_id) REFERENCES CountSessions(session_id),
FOREIGN KEY (location_count_id) REFERENCES LocationCounts(location_count_id),
FOREIGN KEY (marked_by) REFERENCES Users(user_id)
)
''')
# ============================================
# MODULE SYSTEM TABLES
# ============================================
# Modules Table - Available feature modules
cursor.execute('''
CREATE TABLE IF NOT EXISTS Modules (
module_id INTEGER PRIMARY KEY AUTOINCREMENT,
module_name TEXT NOT NULL,
module_key TEXT UNIQUE NOT NULL,
description TEXT,
icon TEXT,
is_active INTEGER DEFAULT 1,
display_order INTEGER DEFAULT 0
)
''')
# UserModules Table - Module access per user
cursor.execute('''
CREATE TABLE IF NOT EXISTS UserModules (
user_module_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
module_id INTEGER NOT NULL,
granted_by INTEGER,
granted_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES Users(user_id),
FOREIGN KEY (module_id) REFERENCES Modules(module_id),
FOREIGN KEY (granted_by) REFERENCES Users(user_id),
UNIQUE(user_id, module_id)
)
''')
# ============================================
# CONSUMPTION SHEETS MODULE TABLES
# ============================================
# cons_processes - Master list of consumption sheet process types
cursor.execute('''
CREATE TABLE IF NOT EXISTS cons_processes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
process_key TEXT UNIQUE NOT NULL,
process_name TEXT NOT NULL,
template_file BLOB,
template_filename TEXT,
rows_per_page INTEGER DEFAULT 30,
detail_start_row INTEGER DEFAULT 10,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER NOT NULL,
is_active INTEGER DEFAULT 1,
FOREIGN KEY (created_by) REFERENCES Users(user_id)
)
''')
# cons_process_fields - Custom field definitions for each process
cursor.execute('''
CREATE TABLE IF NOT EXISTS cons_process_fields (
id INTEGER PRIMARY KEY AUTOINCREMENT,
process_id INTEGER NOT NULL,
table_type TEXT NOT NULL CHECK(table_type IN ('header', 'detail')),
field_name TEXT NOT NULL,
field_label TEXT NOT NULL,
field_type TEXT NOT NULL CHECK(field_type IN ('TEXT', 'INTEGER', 'REAL', 'DATE', 'DATETIME')),
max_length INTEGER,
is_required INTEGER DEFAULT 0,
is_duplicate_key INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
sort_order INTEGER DEFAULT 0,
excel_cell TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (process_id) REFERENCES cons_processes(id)
)
''')
# cons_sessions - Staff scanning sessions
cursor.execute('''
CREATE TABLE IF NOT EXISTS cons_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
process_id INTEGER NOT NULL,
created_by INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived')),
FOREIGN KEY (process_id) REFERENCES cons_processes(id),
FOREIGN KEY (created_by) REFERENCES Users(user_id)
)
''')
# Note: Header values still use flexible key-value storage
# cons_session_header_values - Flexible storage for header field values
cursor.execute('''
CREATE TABLE IF NOT EXISTS cons_session_header_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id INTEGER NOT NULL,
field_id INTEGER NOT NULL,
field_value TEXT,
FOREIGN KEY (session_id) REFERENCES cons_sessions(id),
FOREIGN KEY (field_id) REFERENCES cons_process_fields(id)
)
''')
# Note: Detail tables are created dynamically per process as cons_proc_{process_key}_details
# They include system columns (id, session_id, scanned_by, scanned_at, duplicate_status,
# duplicate_info, comment, is_deleted) plus custom fields defined in cons_process_fields
# Create Indexes
# MASTER baseline indexes
cursor.execute('CREATE INDEX IF NOT EXISTS idx_baseline_master_lot ON BaselineInventory_Master(session_id, lot_number)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_baseline_master_loc ON BaselineInventory_Master(session_id, system_location)')
# ScanEntries indexes
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scanentries_session ON ScanEntries(session_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scanentries_location ON ScanEntries(location_count_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scanentries_lot ON ScanEntries(lot_number)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scanentries_deleted ON ScanEntries(is_deleted)')
# LocationCounts indexes
cursor.execute('CREATE INDEX IF NOT EXISTS idx_location_counts ON LocationCounts(session_id, status)')
# Note: No indexes on BaselineInventory_Current needed - UNIQUE constraint handles lookups
# Consumption Sheets indexes
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_process_fields_process ON cons_process_fields(process_id, table_type)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_process_fields_active ON cons_process_fields(process_id, is_active)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_sessions_process ON cons_sessions(process_id, status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_sessions_user ON cons_sessions(created_by, status)')
# Note: Detail table indexes are created dynamically when process tables are created
conn.commit()
conn.close()
print(f"✅ Database initialized at: {DB_PATH}")
print("📝 Schema version: Post-migration (CURRENT baseline is global)")
def create_default_users():
"""Create default users for testing"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
default_users = [
('owner', generate_password_hash('owner123'), 'System Owner', 'owner'),
('admin', generate_password_hash('admin123'), 'Admin User', 'admin'),
('staff1', generate_password_hash('staff123'), 'John Doe', 'staff'),
('staff2', generate_password_hash('staff123'), 'Jane Smith', 'staff'),
]
try:
cursor.executemany('''
INSERT INTO Users (username, password, full_name, role)
VALUES (?, ?, ?, ?)
''', default_users)
conn.commit()
print("✅ Default users created:")
print(" Owner: owner / owner123")
print(" Admin: admin / admin123")
print(" Staff: staff1 / staff123")
print(" Staff: staff2 / staff123")
except sqlite3.IntegrityError:
print(" Default users already exist")
conn.close()
def create_default_modules():
"""Create default modules and assign to admin users"""
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Define default modules
default_modules = [
('Inventory Counts', 'counting', 'Cycle counts and physical inventory', 'fa-clipboard-check', 1, 1),
('Consumption Sheets', 'cons_sheets', 'Production consumption tracking', 'fa-clipboard-list', 1, 2),
]
# Insert modules (ignore if already exist)
for module in default_modules:
try:
cursor.execute('''
INSERT INTO Modules (module_name, module_key, description, icon, is_active, display_order)
VALUES (?, ?, ?, ?, ?, ?)
''', module)
except sqlite3.IntegrityError:
pass # Module already exists
conn.commit()
# Auto-assign all modules to owner and admin users
cursor.execute('SELECT user_id FROM Users WHERE role IN ("owner", "admin")')
admin_users = cursor.fetchall()
cursor.execute('SELECT module_id FROM Modules')
all_modules = cursor.fetchall()
for user in admin_users:
for module in all_modules:
try:
cursor.execute('''
INSERT INTO UserModules (user_id, module_id)
VALUES (?, ?)
''', (user[0], module[0]))
except sqlite3.IntegrityError:
pass # Assignment already exists
conn.commit()
conn.close()
print("✅ Default modules created and assigned to admin users")
if __name__ == '__main__':
init_database()
create_default_users()
create_default_modules()