Initial commit
This commit is contained in:
commit
983cee0320
322 changed files with 57174 additions and 0 deletions
54
migrations/add_category_budgets.py
Normal file
54
migrations/add_category_budgets.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
Migration: Add budget tracking fields to categories
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def migrate():
|
||||
"""Add budget fields to categories table"""
|
||||
|
||||
db_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'fina.db')
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database not found at {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Add monthly_budget to categories
|
||||
print("Adding monthly_budget column to categories table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE categories
|
||||
ADD COLUMN monthly_budget REAL
|
||||
""")
|
||||
print("✓ Added monthly_budget to categories")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e).lower():
|
||||
print("✓ monthly_budget column already exists in categories")
|
||||
else:
|
||||
print(f"Error adding monthly_budget to categories: {e}")
|
||||
|
||||
try:
|
||||
# Add budget_alert_threshold to categories
|
||||
print("Adding budget_alert_threshold column to categories table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE categories
|
||||
ADD COLUMN budget_alert_threshold REAL DEFAULT 0.9
|
||||
""")
|
||||
print("✓ Added budget_alert_threshold to categories")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e).lower():
|
||||
print("✓ budget_alert_threshold column already exists in categories")
|
||||
else:
|
||||
print(f"Error adding budget_alert_threshold to categories: {e}")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("\n✓ Budget tracking migration completed successfully!")
|
||||
print("Categories can now have monthly budgets with customizable alert thresholds.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate()
|
||||
58
migrations/add_income.py
Normal file
58
migrations/add_income.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""
|
||||
Migration to add Income model for tracking income sources
|
||||
Run with: python migrations/add_income.py
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def migrate():
|
||||
"""Add income table"""
|
||||
|
||||
db_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'fina.db')
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database not found at {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if table already exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='income'")
|
||||
if cursor.fetchone():
|
||||
print("✓ Income table already exists")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Create income table
|
||||
cursor.execute("""
|
||||
CREATE TABLE income (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
amount REAL NOT NULL,
|
||||
currency VARCHAR(3) DEFAULT 'USD',
|
||||
description VARCHAR(200) NOT NULL,
|
||||
source VARCHAR(100) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
tags TEXT DEFAULT '[]',
|
||||
date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
print("✓ Created income table")
|
||||
print("✓ Migration completed successfully")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"✗ Migration failed: {e}")
|
||||
conn.rollback()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Starting income migration...")
|
||||
migrate()
|
||||
59
migrations/add_income_frequency.py
Normal file
59
migrations/add_income_frequency.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""
|
||||
Migration to add frequency fields to Income model
|
||||
Run with: python migrations/add_income_frequency.py
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def migrate():
|
||||
"""Add frequency and custom_days columns to income table"""
|
||||
|
||||
db_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'fina.db')
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database not found at {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check if columns already exist
|
||||
cursor.execute("PRAGMA table_info(income)")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
# Add frequency column if it doesn't exist
|
||||
if 'frequency' not in columns:
|
||||
print("Adding frequency column to income table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE income
|
||||
ADD COLUMN frequency VARCHAR(50) DEFAULT 'once'
|
||||
""")
|
||||
print("✓ Added frequency column")
|
||||
else:
|
||||
print("✓ Frequency column already exists")
|
||||
|
||||
# Add custom_days column if it doesn't exist
|
||||
if 'custom_days' not in columns:
|
||||
print("Adding custom_days column to income table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE income
|
||||
ADD COLUMN custom_days INTEGER
|
||||
""")
|
||||
print("✓ Added custom_days column")
|
||||
else:
|
||||
print("✓ Custom_days column already exists")
|
||||
|
||||
conn.commit()
|
||||
print("\n✓ Income frequency migration completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
print(f"\n✗ Migration failed: {str(e)}")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Starting income frequency migration...")
|
||||
migrate()
|
||||
28
migrations/add_monthly_budget.py
Normal file
28
migrations/add_monthly_budget.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""
|
||||
Migration: Add monthly_budget column to users table
|
||||
Run this with: python migrations/add_monthly_budget.py
|
||||
"""
|
||||
|
||||
from app import create_app, db
|
||||
|
||||
def migrate():
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
try:
|
||||
# Check if column exists
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(db.engine)
|
||||
columns = [col['name'] for col in inspector.get_columns('users')]
|
||||
|
||||
if 'monthly_budget' not in columns:
|
||||
db.engine.execute('ALTER TABLE users ADD COLUMN monthly_budget FLOAT DEFAULT 0.0')
|
||||
print("✅ Successfully added monthly_budget column to users table")
|
||||
else:
|
||||
print("ℹ️ Column monthly_budget already exists")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Migration failed: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate()
|
||||
56
migrations/add_ocr_fields.py
Normal file
56
migrations/add_ocr_fields.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""
|
||||
Migration: Add OCR text fields to expenses and documents tables
|
||||
Run this after updating models to add OCR support
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
def migrate():
|
||||
"""Add ocr_text columns to documents and expenses tables"""
|
||||
|
||||
# Connect to database
|
||||
db_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'fina.db')
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Database not found at {db_path}")
|
||||
return
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Add ocr_text to documents table
|
||||
print("Adding ocr_text column to documents table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE documents
|
||||
ADD COLUMN ocr_text TEXT
|
||||
""")
|
||||
print("✓ Added ocr_text to documents")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e).lower():
|
||||
print("✓ ocr_text column already exists in documents")
|
||||
else:
|
||||
print(f"Error adding ocr_text to documents: {e}")
|
||||
|
||||
try:
|
||||
# Add receipt_ocr_text to expenses table
|
||||
print("Adding receipt_ocr_text column to expenses table...")
|
||||
cursor.execute("""
|
||||
ALTER TABLE expenses
|
||||
ADD COLUMN receipt_ocr_text TEXT
|
||||
""")
|
||||
print("✓ Added receipt_ocr_text to expenses")
|
||||
except sqlite3.OperationalError as e:
|
||||
if "duplicate column name" in str(e).lower():
|
||||
print("✓ receipt_ocr_text column already exists in expenses")
|
||||
else:
|
||||
print(f"Error adding receipt_ocr_text to expenses: {e}")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("\n✓ Migration completed successfully!")
|
||||
print("OCR functionality is now enabled for documents and receipts.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate()
|
||||
16
migrations/add_recurring_expenses.py
Normal file
16
migrations/add_recurring_expenses.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
Migration script to add recurring_expenses table to the database
|
||||
Run this script to update the database schema
|
||||
"""
|
||||
from app import create_app, db
|
||||
from app.models import RecurringExpense
|
||||
|
||||
def migrate():
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
# Create the recurring_expenses table
|
||||
db.create_all()
|
||||
print("✓ Migration complete: recurring_expenses table created")
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate()
|
||||
95
migrations/add_recurring_income.py
Normal file
95
migrations/add_recurring_income.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to add recurring income fields to Income table
|
||||
Adds: next_due_date, last_created_date, is_active, auto_create
|
||||
Idempotent: Can be run multiple times safely
|
||||
"""
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
def migrate():
|
||||
"""Add recurring fields to income table"""
|
||||
# Try both possible database locations
|
||||
db_paths = ['data/fina.db', 'instance/fina.db']
|
||||
conn = None
|
||||
|
||||
for db_path in db_paths:
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
# Test if we can access the database
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
if 'income' in tables:
|
||||
print(f"Using database at: {db_path}")
|
||||
break
|
||||
else:
|
||||
conn.close()
|
||||
conn = None
|
||||
except:
|
||||
if conn:
|
||||
conn.close()
|
||||
conn = None
|
||||
continue
|
||||
|
||||
if not conn:
|
||||
print("Error: Could not find fina.db with income table")
|
||||
return
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check what columns exist
|
||||
cursor.execute("PRAGMA table_info(income)")
|
||||
existing_columns = [row[1] for row in cursor.fetchall()]
|
||||
print(f"Existing columns in income table: {existing_columns}")
|
||||
|
||||
# Add next_due_date column if it doesn't exist
|
||||
if 'next_due_date' not in existing_columns:
|
||||
print("Adding next_due_date column...")
|
||||
cursor.execute('''
|
||||
ALTER TABLE income ADD COLUMN next_due_date DATETIME
|
||||
''')
|
||||
print("✓ Added next_due_date column")
|
||||
else:
|
||||
print("✓ next_due_date column already exists")
|
||||
|
||||
# Add last_created_date column if it doesn't exist
|
||||
if 'last_created_date' not in existing_columns:
|
||||
print("Adding last_created_date column...")
|
||||
cursor.execute('''
|
||||
ALTER TABLE income ADD COLUMN last_created_date DATETIME
|
||||
''')
|
||||
print("✓ Added last_created_date column")
|
||||
else:
|
||||
print("✓ last_created_date column already exists")
|
||||
|
||||
# Add is_active column if it doesn't exist
|
||||
if 'is_active' not in existing_columns:
|
||||
print("Adding is_active column...")
|
||||
cursor.execute('''
|
||||
ALTER TABLE income ADD COLUMN is_active BOOLEAN DEFAULT 1
|
||||
''')
|
||||
print("✓ Added is_active column")
|
||||
else:
|
||||
print("✓ is_active column already exists")
|
||||
|
||||
# Add auto_create column if it doesn't exist
|
||||
if 'auto_create' not in existing_columns:
|
||||
print("Adding auto_create column...")
|
||||
cursor.execute('''
|
||||
ALTER TABLE income ADD COLUMN auto_create BOOLEAN DEFAULT 0
|
||||
''')
|
||||
print("✓ Added auto_create column")
|
||||
else:
|
||||
print("✓ auto_create column already exists")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("\n✓ Migration completed successfully!")
|
||||
print("Recurring income fields added to Income table")
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Starting migration: add_recurring_income.py")
|
||||
print("=" * 60)
|
||||
migrate()
|
||||
78
migrations/add_smart_tags.py
Normal file
78
migrations/add_smart_tags.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""
|
||||
Migration: Add Smart Tags System
|
||||
Creates Tag and ExpenseTag tables for smart expense tagging
|
||||
"""
|
||||
from app import create_app, db
|
||||
from sqlalchemy import text
|
||||
|
||||
def upgrade():
|
||||
"""Create Tag and ExpenseTag tables"""
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
# Create Tag table
|
||||
db.session.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
color VARCHAR(7) DEFAULT '#6366f1',
|
||||
icon VARCHAR(50) DEFAULT 'label',
|
||||
user_id INTEGER NOT NULL,
|
||||
is_auto BOOLEAN DEFAULT 0,
|
||||
use_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||
UNIQUE (name, user_id)
|
||||
)
|
||||
"""))
|
||||
|
||||
# Create ExpenseTag junction table
|
||||
db.session.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS expense_tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
expense_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (expense_id) REFERENCES expenses (id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE,
|
||||
UNIQUE (expense_id, tag_id)
|
||||
)
|
||||
"""))
|
||||
|
||||
# Create indexes for performance
|
||||
db.session.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_user_id ON tags(user_id)
|
||||
"""))
|
||||
|
||||
db.session.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)
|
||||
"""))
|
||||
|
||||
db.session.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_expense_tags_expense_id ON expense_tags(expense_id)
|
||||
"""))
|
||||
|
||||
db.session.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_expense_tags_tag_id ON expense_tags(tag_id)
|
||||
"""))
|
||||
|
||||
db.session.commit()
|
||||
print("✓ Smart Tags tables created successfully")
|
||||
|
||||
|
||||
def downgrade():
|
||||
"""Remove Tag and ExpenseTag tables"""
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
db.session.execute(text("DROP TABLE IF EXISTS expense_tags"))
|
||||
db.session.execute(text("DROP TABLE IF EXISTS tags"))
|
||||
db.session.commit()
|
||||
print("✓ Smart Tags tables removed")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Running migration: Add Smart Tags System")
|
||||
upgrade()
|
||||
print("Migration completed successfully!")
|
||||
134
migrations/backfill_ocr.py
Normal file
134
migrations/backfill_ocr.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
Backfill OCR text for existing documents and receipts
|
||||
This will process all uploaded files that don't have OCR text yet
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from app import create_app, db
|
||||
from app.models import Document, Expense
|
||||
from app.ocr import extract_text_from_file
|
||||
|
||||
app = create_app()
|
||||
|
||||
def process_documents():
|
||||
"""Process all documents without OCR text"""
|
||||
with app.app_context():
|
||||
# Find documents without OCR text
|
||||
documents = Document.query.filter(
|
||||
(Document.ocr_text == None) | (Document.ocr_text == '')
|
||||
).all()
|
||||
|
||||
print(f"\nFound {len(documents)} documents to process")
|
||||
|
||||
processed = 0
|
||||
errors = 0
|
||||
|
||||
for doc in documents:
|
||||
try:
|
||||
# Check if file type supports OCR
|
||||
if doc.file_type.lower() not in ['pdf', 'png', 'jpg', 'jpeg']:
|
||||
print(f"⊘ Skipping {doc.original_filename} - {doc.file_type} not supported for OCR")
|
||||
continue
|
||||
|
||||
# Get absolute file path
|
||||
file_path = os.path.abspath(doc.file_path)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"✗ File not found: {doc.original_filename}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
print(f"Processing: {doc.original_filename}...", end=' ')
|
||||
|
||||
# Extract OCR text
|
||||
ocr_text = extract_text_from_file(file_path, doc.file_type)
|
||||
|
||||
if ocr_text:
|
||||
doc.ocr_text = ocr_text
|
||||
db.session.commit()
|
||||
print(f"✓ Extracted {len(ocr_text)} characters")
|
||||
processed += 1
|
||||
else:
|
||||
print("⊘ No text found")
|
||||
# Still update to empty string to mark as processed
|
||||
doc.ocr_text = ""
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {str(e)}")
|
||||
errors += 1
|
||||
|
||||
print(f"\n✓ Documents processed: {processed}")
|
||||
print(f"⊘ Documents with no text: {len(documents) - processed - errors}")
|
||||
print(f"✗ Errors: {errors}")
|
||||
|
||||
|
||||
def process_receipts():
|
||||
"""Process all expense receipts without OCR text"""
|
||||
with app.app_context():
|
||||
# Find expenses with receipts but no OCR text
|
||||
expenses = Expense.query.filter(
|
||||
Expense.receipt_path != None,
|
||||
(Expense.receipt_ocr_text == None) | (Expense.receipt_ocr_text == '')
|
||||
).all()
|
||||
|
||||
print(f"\nFound {len(expenses)} receipts to process")
|
||||
|
||||
processed = 0
|
||||
errors = 0
|
||||
|
||||
for expense in expenses:
|
||||
try:
|
||||
# Build absolute path
|
||||
receipt_path = expense.receipt_path.replace('/uploads/', '').lstrip('/')
|
||||
file_path = os.path.abspath(os.path.join('/app', 'uploads', receipt_path))
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"✗ Receipt not found for: {expense.description}")
|
||||
errors += 1
|
||||
continue
|
||||
|
||||
# Get file extension
|
||||
file_ext = file_path.rsplit('.', 1)[1].lower() if '.' in file_path else ''
|
||||
|
||||
if file_ext not in ['pdf', 'png', 'jpg', 'jpeg']:
|
||||
print(f"⊘ Skipping receipt for {expense.description} - {file_ext} not supported")
|
||||
continue
|
||||
|
||||
print(f"Processing receipt for: {expense.description}...", end=' ')
|
||||
|
||||
# Extract OCR text
|
||||
ocr_text = extract_text_from_file(file_path, file_ext)
|
||||
|
||||
if ocr_text:
|
||||
expense.receipt_ocr_text = ocr_text
|
||||
db.session.commit()
|
||||
print(f"✓ Extracted {len(ocr_text)} characters")
|
||||
processed += 1
|
||||
else:
|
||||
print("⊘ No text found")
|
||||
expense.receipt_ocr_text = ""
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error: {str(e)}")
|
||||
errors += 1
|
||||
|
||||
print(f"\n✓ Receipts processed: {processed}")
|
||||
print(f"⊘ Receipts with no text: {len(expenses) - processed - errors}")
|
||||
print(f"✗ Errors: {errors}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("=" * 60)
|
||||
print("OCR BACKFILL - Processing existing files")
|
||||
print("=" * 60)
|
||||
|
||||
process_documents()
|
||||
process_receipts()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✓ OCR backfill completed!")
|
||||
print("=" * 60)
|
||||
Loading…
Add table
Add a link
Reference in a new issue