Initial commit
This commit is contained in:
commit
983cee0320
322 changed files with 57174 additions and 0 deletions
3
backup/first -fina app/app/models/__init__.py
Executable file
3
backup/first -fina app/app/models/__init__.py
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
from app.models.user import User
|
||||
from app.models.category import Category, Expense
|
||||
__all__ = ['User', 'Category', 'Expense']
|
||||
120
backup/first -fina app/app/models/category.py
Executable file
120
backup/first -fina app/app/models/category.py
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func, extract
|
||||
|
||||
class Category(db.Model):
|
||||
__tablename__ = 'categories'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
color = db.Column(db.String(7), default='#6366f1')
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# Budget fields
|
||||
monthly_budget = db.Column(db.Float, nullable=True)
|
||||
budget_alert_sent = db.Column(db.Boolean, default=False)
|
||||
budget_alert_threshold = db.Column(db.Float, default=1.0) # 1.0 = 100%
|
||||
last_budget_check = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
expenses = db.relationship('Expense', backref='category', lazy=True, cascade='all, delete-orphan')
|
||||
|
||||
def get_total_spent(self):
|
||||
return sum(expense.amount for expense in self.expenses)
|
||||
|
||||
def get_monthly_totals(self, year=None):
|
||||
"""Get expenses grouped by month for the year"""
|
||||
if year is None:
|
||||
year = datetime.now().year
|
||||
|
||||
monthly_data = db.session.query(
|
||||
extract('month', Expense.date).label('month'),
|
||||
func.sum(Expense.amount).label('total')
|
||||
).filter(
|
||||
Expense.category_id == self.id,
|
||||
extract('year', Expense.date) == year
|
||||
).group_by('month').all()
|
||||
|
||||
# Create array with all 12 months
|
||||
result = [0] * 12
|
||||
for month, total in monthly_data:
|
||||
result[int(month) - 1] = float(total) if total else 0
|
||||
|
||||
return result
|
||||
|
||||
def get_yearly_total(self, year):
|
||||
"""Get total expenses for a specific year"""
|
||||
total = db.session.query(func.sum(Expense.amount)).filter(
|
||||
Expense.category_id == self.id,
|
||||
extract('year', Expense.date) == year
|
||||
).scalar()
|
||||
return float(total) if total else 0
|
||||
|
||||
def get_current_month_spending(self):
|
||||
"""Get total spending for current month"""
|
||||
now = datetime.now()
|
||||
total = db.session.query(func.sum(Expense.amount)).filter(
|
||||
Expense.category_id == self.id,
|
||||
extract('year', Expense.date) == now.year,
|
||||
extract('month', Expense.date) == now.month
|
||||
).scalar()
|
||||
return float(total) if total else 0
|
||||
|
||||
def get_budget_status(self):
|
||||
"""Get budget status: percentage used and over budget flag"""
|
||||
if not self.monthly_budget or self.monthly_budget <= 0:
|
||||
return {'percentage': 0, 'over_budget': False, 'remaining': 0}
|
||||
|
||||
spent = self.get_current_month_spending()
|
||||
percentage = (spent / self.monthly_budget) * 100
|
||||
over_budget = percentage >= (self.budget_alert_threshold * 100)
|
||||
remaining = self.monthly_budget - spent
|
||||
|
||||
return {
|
||||
'spent': spent,
|
||||
'budget': self.monthly_budget,
|
||||
'percentage': round(percentage, 1),
|
||||
'over_budget': over_budget,
|
||||
'remaining': remaining
|
||||
}
|
||||
|
||||
def should_send_budget_alert(self):
|
||||
"""Check if budget alert should be sent"""
|
||||
if not self.monthly_budget:
|
||||
return False
|
||||
|
||||
status = self.get_budget_status()
|
||||
|
||||
# Only send if over threshold and not already sent this month
|
||||
if status['over_budget'] and not self.budget_alert_sent:
|
||||
return True
|
||||
|
||||
# Reset alert flag at start of new month
|
||||
now = datetime.now()
|
||||
if self.last_budget_check:
|
||||
if (self.last_budget_check.month != now.month or
|
||||
self.last_budget_check.year != now.year):
|
||||
self.budget_alert_sent = False
|
||||
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Category {self.name}>'
|
||||
|
||||
class Expense(db.Model):
|
||||
__tablename__ = 'expenses'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
description = db.Column(db.String(200), nullable=False)
|
||||
amount = db.Column(db.Float, nullable=False)
|
||||
date = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
paid_by = db.Column(db.String(100))
|
||||
tags = db.Column(db.String(500))
|
||||
file_path = db.Column(db.String(500))
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Expense {self.description}: ${self.amount}>'
|
||||
4
backup/first -fina app/app/models/init.py
Executable file
4
backup/first -fina app/app/models/init.py
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
from app.models.user import User
|
||||
from app.models.category import Category, Expense
|
||||
|
||||
__all__ = ['User', 'Category', 'Expense']
|
||||
124
backup/first -fina app/app/models/subscription.py
Normal file
124
backup/first -fina app/app/models/subscription.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
from app import db
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func
|
||||
|
||||
class Subscription(db.Model):
|
||||
__tablename__ = 'subscriptions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
amount = db.Column(db.Float, nullable=False)
|
||||
frequency = db.Column(db.String(20), nullable=False) # monthly, weekly, yearly, quarterly, custom
|
||||
custom_interval_days = db.Column(db.Integer, nullable=True) # For custom frequency
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
next_due_date = db.Column(db.Date, nullable=True)
|
||||
start_date = db.Column(db.Date, nullable=True) # First occurrence date
|
||||
end_date = db.Column(db.Date, nullable=True) # Optional end date
|
||||
total_occurrences = db.Column(db.Integer, nullable=True) # Optional limit
|
||||
occurrences_count = db.Column(db.Integer, default=0) # Current count
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
is_confirmed = db.Column(db.Boolean, default=False) # User confirmed this subscription
|
||||
auto_detected = db.Column(db.Boolean, default=False) # System detected this pattern
|
||||
auto_create_expense = db.Column(db.Boolean, default=False) # Auto-create expenses on due date
|
||||
confidence_score = db.Column(db.Float, default=0.0) # 0-100 confidence of detection
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_reminded = db.Column(db.DateTime, nullable=True)
|
||||
last_auto_created = db.Column(db.Date, nullable=True) # Last auto-created expense date
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Subscription {self.name}>'
|
||||
|
||||
def get_frequency_days(self):
|
||||
"""Get number of days between payments"""
|
||||
if self.frequency == 'custom' and self.custom_interval_days:
|
||||
return self.custom_interval_days
|
||||
|
||||
frequency_map = {
|
||||
'weekly': 7,
|
||||
'biweekly': 14,
|
||||
'monthly': 30,
|
||||
'quarterly': 90,
|
||||
'yearly': 365
|
||||
}
|
||||
return frequency_map.get(self.frequency, 30)
|
||||
|
||||
def should_create_expense_today(self):
|
||||
"""Check if an expense should be auto-created today"""
|
||||
if not self.auto_create_expense or not self.is_active:
|
||||
return False
|
||||
|
||||
if not self.next_due_date:
|
||||
return False
|
||||
|
||||
today = datetime.now().date()
|
||||
|
||||
# Check if today is the due date
|
||||
if self.next_due_date != today:
|
||||
return False
|
||||
|
||||
# Check if already created today
|
||||
if self.last_auto_created == today:
|
||||
return False
|
||||
|
||||
# Check if we've reached the occurrence limit
|
||||
if self.total_occurrences and self.occurrences_count >= self.total_occurrences:
|
||||
return False
|
||||
|
||||
# Check if past end date
|
||||
if self.end_date and today > self.end_date:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def advance_next_due_date(self):
|
||||
"""Move to the next due date"""
|
||||
if not self.next_due_date:
|
||||
return
|
||||
|
||||
from datetime import timedelta
|
||||
interval_days = self.get_frequency_days()
|
||||
self.next_due_date = self.next_due_date + timedelta(days=interval_days)
|
||||
self.occurrences_count += 1
|
||||
|
||||
# Check if subscription should end
|
||||
if self.total_occurrences and self.occurrences_count >= self.total_occurrences:
|
||||
self.is_active = False
|
||||
|
||||
if self.end_date and self.next_due_date > self.end_date:
|
||||
self.is_active = False
|
||||
|
||||
def get_annual_cost(self):
|
||||
"""Calculate annual cost based on frequency"""
|
||||
frequency_multiplier = {
|
||||
'weekly': 52,
|
||||
'biweekly': 26,
|
||||
'monthly': 12,
|
||||
'quarterly': 4,
|
||||
'yearly': 1
|
||||
}
|
||||
return self.amount * frequency_multiplier.get(self.frequency, 12)
|
||||
|
||||
|
||||
class RecurringPattern(db.Model):
|
||||
"""Detected recurring patterns (suggestions before confirmation)"""
|
||||
__tablename__ = 'recurring_patterns'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
|
||||
suggested_name = db.Column(db.String(100), nullable=False)
|
||||
average_amount = db.Column(db.Float, nullable=False)
|
||||
detected_frequency = db.Column(db.String(20), nullable=False)
|
||||
confidence_score = db.Column(db.Float, nullable=False) # 0-100
|
||||
expense_ids = db.Column(db.Text, nullable=False) # JSON array of expense IDs
|
||||
first_occurrence = db.Column(db.Date, nullable=False)
|
||||
last_occurrence = db.Column(db.Date, nullable=False)
|
||||
occurrence_count = db.Column(db.Integer, default=0)
|
||||
is_dismissed = db.Column(db.Boolean, default=False)
|
||||
is_converted = db.Column(db.Boolean, default=False) # Converted to subscription
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<RecurringPattern {self.suggested_name}>'
|
||||
71
backup/first -fina app/app/models/user.py
Executable file
71
backup/first -fina app/app/models/user.py
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
import pyotp
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(200), nullable=False)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
currency = db.Column(db.String(3), default='USD')
|
||||
language = db.Column(db.String(2), default='en') # en, ro, es
|
||||
|
||||
# Budget alert preferences
|
||||
budget_alerts_enabled = db.Column(db.Boolean, default=True)
|
||||
alert_email = db.Column(db.String(120), nullable=True) # Optional separate alert email
|
||||
|
||||
# 2FA fields
|
||||
totp_secret = db.Column(db.String(32), nullable=True)
|
||||
is_2fa_enabled = db.Column(db.Boolean, default=False)
|
||||
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
categories = db.relationship('Category', backref='user', lazy=True, cascade='all, delete-orphan')
|
||||
expenses = db.relationship('Expense', backref='user', lazy=True, cascade='all, delete-orphan')
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def generate_totp_secret(self):
|
||||
"""Generate a new TOTP secret"""
|
||||
self.totp_secret = pyotp.random_base32()
|
||||
return self.totp_secret
|
||||
|
||||
def get_totp_uri(self):
|
||||
"""Get TOTP URI for QR code"""
|
||||
if not self.totp_secret:
|
||||
self.generate_totp_secret()
|
||||
return pyotp.totp.TOTP(self.totp_secret).provisioning_uri(
|
||||
name=self.email,
|
||||
issuer_name='FINA'
|
||||
)
|
||||
|
||||
def verify_totp(self, token):
|
||||
"""Verify TOTP token"""
|
||||
if not self.totp_secret:
|
||||
return False
|
||||
totp = pyotp.TOTP(self.totp_secret)
|
||||
return totp.verify(token, valid_window=1)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
class Tag(db.Model):
|
||||
__tablename__ = 'tags'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50), nullable=False)
|
||||
color = db.Column(db.String(7), default='#6366f1')
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Tag {self.name}>'
|
||||
Loading…
Add table
Add a link
Reference in a new issue