05/31 Phase 1: initial codes
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from app.models.user import User
|
||||
from app.models.account import Account
|
||||
from app.models.category import Category
|
||||
from app.models.receipt import Receipt
|
||||
from app.models.recurring_rule import RecurringRule
|
||||
from app.models.transaction import Transaction
|
||||
from app.models.budget import Budget
|
||||
from app.models.goal import Goal, GoalContribution
|
||||
from app.models.investment import Investment, InvestmentTransaction
|
||||
from app.models.net_worth_snapshot import NetWorthSnapshot
|
||||
from app.models.ai_insight import AiInsight
|
||||
from app.models.fx_rate import FxRate
|
||||
@@ -0,0 +1,32 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Account(db.Model):
|
||||
__tablename__ = 'accounts'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
account_type = db.Column(
|
||||
db.Enum('checking', 'savings', 'cash', 'credit_card', 'crypto', 'investment', 'other'),
|
||||
nullable=False,
|
||||
default='checking'
|
||||
)
|
||||
balance = db.Column(db.Numeric(15, 2), default=0.00, nullable=False)
|
||||
credit_limit = db.Column(db.Numeric(15, 2), nullable=True) # credit cards
|
||||
due_date = db.Column(db.Integer, nullable=True) # day of month bill due
|
||||
color = db.Column(db.String(7), default='#4F81C7') # hex color for UI
|
||||
icon = db.Column(db.String(50), default='bi-bank') # bootstrap icon name
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
transactions = db.relationship('Transaction', back_populates='account',
|
||||
lazy='dynamic', foreign_keys='Transaction.account_id')
|
||||
transfer_transactions = db.relationship('Transaction', back_populates='to_account',
|
||||
lazy='dynamic', foreign_keys='Transaction.to_account_id')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Account {self.name}>'
|
||||
@@ -0,0 +1,22 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class AiInsight(db.Model):
|
||||
__tablename__ = 'ai_insights'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
insight_date = db.Column(db.Date, nullable=False, index=True)
|
||||
insight_type = db.Column(
|
||||
db.Enum('daily_summary', 'weekly_summary', 'chat_response', 'alert'),
|
||||
nullable=False,
|
||||
default='daily_summary'
|
||||
)
|
||||
prompt_summary = db.Column(db.Text, nullable=True) # what context was sent
|
||||
content = db.Column(db.Text, nullable=False) # AI response
|
||||
model_used = db.Column(db.String(64), nullable=True)
|
||||
tokens_used = db.Column(db.Integer, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<AiInsight {self.insight_type} {self.insight_date}>'
|
||||
@@ -0,0 +1,23 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Budget(db.Model):
|
||||
__tablename__ = 'budgets'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False)
|
||||
month = db.Column(db.String(7), nullable=False, index=True) # YYYY-MM
|
||||
limit_amount = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
rollover_enabled = db.Column(db.Boolean, default=False)
|
||||
rollover_amount = db.Column(db.Numeric(15, 2), default=0.00)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
category = db.relationship('Category')
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('category_id', 'month', name='uq_budget_category_month'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Budget {self.month} cat={self.category_id} limit={self.limit_amount}>'
|
||||
@@ -0,0 +1,28 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Category(db.Model):
|
||||
__tablename__ = 'categories'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
category_type = db.Column(
|
||||
db.Enum('income', 'expense', 'both'),
|
||||
nullable=False,
|
||||
default='expense'
|
||||
)
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
|
||||
color = db.Column(db.String(7), default='#6B7280')
|
||||
icon = db.Column(db.String(50), default='bi-tag')
|
||||
is_system = db.Column(db.Boolean, default=False) # system categories can't be deleted
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
# Self-referential relationship for subcategories
|
||||
subcategories = db.relationship('Category', backref=db.backref('parent', remote_side=[id]),
|
||||
lazy='dynamic')
|
||||
transactions = db.relationship('Transaction', back_populates='category', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Category {self.name}>'
|
||||
@@ -0,0 +1,15 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class FxRate(db.Model):
|
||||
__tablename__ = 'fx_rates'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.Date, nullable=False, unique=True, index=True)
|
||||
usd_to_vnd = db.Column(db.Numeric(12, 2), nullable=False)
|
||||
source = db.Column(db.String(50), nullable=True) # 'exchangerate-api' | 'vcb' | 'manual'
|
||||
fetched_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<FxRate {self.date} 1USD={self.usd_to_vnd}VND>'
|
||||
@@ -0,0 +1,50 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Goal(db.Model):
|
||||
__tablename__ = 'goals'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
target_amount = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
current_amount = db.Column(db.Numeric(15, 2), default=0.00)
|
||||
target_date = db.Column(db.Date, nullable=True)
|
||||
linked_account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=True)
|
||||
color = db.Column(db.String(7), default='#10B981')
|
||||
icon = db.Column(db.String(50), default='bi-piggy-bank')
|
||||
is_completed = db.Column(db.Boolean, default=False)
|
||||
completed_at = db.Column(db.DateTime, nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
linked_account = db.relationship('Account')
|
||||
contributions = db.relationship('GoalContribution', back_populates='goal',
|
||||
lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
@property
|
||||
def progress_percent(self):
|
||||
if self.target_amount <= 0:
|
||||
return 0
|
||||
pct = (float(self.current_amount) / float(self.target_amount)) * 100
|
||||
return min(round(pct, 1), 100.0)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Goal {self.name}>'
|
||||
|
||||
|
||||
class GoalContribution(db.Model):
|
||||
__tablename__ = 'goal_contributions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
goal_id = db.Column(db.Integer, db.ForeignKey('goals.id'), nullable=False)
|
||||
amount = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
notes = db.Column(db.String(255), nullable=True)
|
||||
date = db.Column(db.Date, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
goal = db.relationship('Goal', back_populates='contributions')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<GoalContribution goal={self.goal_id} amount={self.amount}>'
|
||||
@@ -0,0 +1,69 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Investment(db.Model):
|
||||
__tablename__ = 'investments'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
asset_name = db.Column(db.String(100), nullable=False)
|
||||
ticker = db.Column(db.String(20), nullable=True)
|
||||
asset_type = db.Column(
|
||||
db.Enum('stock', 'etf', 'crypto', 'real_estate', 'bond', 'cash', 'other'),
|
||||
nullable=False,
|
||||
default='stock'
|
||||
)
|
||||
shares = db.Column(db.Numeric(18, 8), nullable=False, default=0)
|
||||
avg_cost_basis = db.Column(db.Numeric(15, 4), nullable=False, default=0)
|
||||
current_price = db.Column(db.Numeric(15, 4), nullable=True)
|
||||
last_price_update = db.Column(db.DateTime, nullable=True)
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
inv_transactions = db.relationship('InvestmentTransaction', back_populates='investment',
|
||||
lazy='dynamic', cascade='all, delete-orphan')
|
||||
|
||||
@property
|
||||
def total_cost(self):
|
||||
return float(self.shares) * float(self.avg_cost_basis)
|
||||
|
||||
@property
|
||||
def current_value(self):
|
||||
if self.current_price:
|
||||
return float(self.shares) * float(self.current_price)
|
||||
return self.total_cost
|
||||
|
||||
@property
|
||||
def unrealized_gain(self):
|
||||
return self.current_value - self.total_cost
|
||||
|
||||
@property
|
||||
def unrealized_gain_pct(self):
|
||||
if self.total_cost == 0:
|
||||
return 0
|
||||
return round((self.unrealized_gain / self.total_cost) * 100, 2)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Investment {self.asset_name}>'
|
||||
|
||||
|
||||
class InvestmentTransaction(db.Model):
|
||||
__tablename__ = 'investment_transactions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
investment_id = db.Column(db.Integer, db.ForeignKey('investments.id'), nullable=False)
|
||||
transaction_type = db.Column(db.Enum('buy', 'sell', 'dividend', 'split'), nullable=False)
|
||||
shares = db.Column(db.Numeric(18, 8), nullable=False)
|
||||
price_per_share = db.Column(db.Numeric(15, 4), nullable=False)
|
||||
total_amount = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
fees = db.Column(db.Numeric(10, 2), default=0.00)
|
||||
date = db.Column(db.Date, nullable=False, index=True)
|
||||
notes = db.Column(db.String(255), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
investment = db.relationship('Investment', back_populates='inv_transactions')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<InvTransaction {self.transaction_type} {self.shares}@{self.price_per_share}>'
|
||||
@@ -0,0 +1,18 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class NetWorthSnapshot(db.Model):
|
||||
__tablename__ = 'net_worth_snapshots'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
snapshot_date = db.Column(db.Date, nullable=False, unique=True, index=True)
|
||||
total_assets = db.Column(db.Numeric(15, 2), nullable=False, default=0)
|
||||
total_liabilities = db.Column(db.Numeric(15, 2), nullable=False, default=0)
|
||||
net_worth = db.Column(db.Numeric(15, 2), nullable=False, default=0)
|
||||
account_balances = db.Column(db.JSON, nullable=True) # snapshot of each account balance
|
||||
investment_value = db.Column(db.Numeric(15, 2), default=0)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<NetWorthSnapshot {self.snapshot_date} nw={self.net_worth}>'
|
||||
@@ -0,0 +1,18 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Receipt(db.Model):
|
||||
__tablename__ = 'receipts'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
filename = db.Column(db.String(255), nullable=False)
|
||||
original_filename = db.Column(db.String(255), nullable=False)
|
||||
file_size = db.Column(db.Integer, nullable=True)
|
||||
mime_type = db.Column(db.String(50), nullable=True)
|
||||
uploaded_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
transaction = db.relationship('Transaction', back_populates='receipt', uselist=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Receipt {self.filename}>'
|
||||
@@ -0,0 +1,35 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class RecurringRule(db.Model):
|
||||
__tablename__ = 'recurring_rules'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=False)
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
|
||||
transaction_type = db.Column(
|
||||
db.Enum('income', 'expense'),
|
||||
nullable=False
|
||||
)
|
||||
amount = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
description = db.Column(db.String(255), nullable=False)
|
||||
frequency = db.Column(
|
||||
db.Enum('daily', 'weekly', 'biweekly', 'monthly', 'quarterly', 'yearly'),
|
||||
nullable=False,
|
||||
default='monthly'
|
||||
)
|
||||
start_date = db.Column(db.Date, nullable=False)
|
||||
end_date = db.Column(db.Date, nullable=True)
|
||||
last_run = db.Column(db.Date, nullable=True)
|
||||
next_run = db.Column(db.Date, nullable=True, index=True)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
transactions = db.relationship('Transaction', back_populates='recurring_rule', lazy='dynamic')
|
||||
account = db.relationship('Account')
|
||||
category = db.relationship('Category')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<RecurringRule {self.name} {self.frequency}>'
|
||||
@@ -0,0 +1,37 @@
|
||||
from app.extensions import db
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Transaction(db.Model):
|
||||
__tablename__ = 'transactions'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=False)
|
||||
to_account_id = db.Column(db.Integer, db.ForeignKey('accounts.id'), nullable=True) # transfers
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
|
||||
recurring_rule_id = db.Column(db.Integer, db.ForeignKey('recurring_rules.id'), nullable=True)
|
||||
receipt_id = db.Column(db.Integer, db.ForeignKey('receipts.id'), nullable=True)
|
||||
|
||||
transaction_type = db.Column(
|
||||
db.Enum('income', 'expense', 'transfer'),
|
||||
nullable=False
|
||||
)
|
||||
amount = db.Column(db.Numeric(15, 2), nullable=False)
|
||||
description = db.Column(db.String(255), nullable=False)
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
date = db.Column(db.Date, nullable=False, index=True)
|
||||
is_recurring = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
account = db.relationship('Account', back_populates='transactions',
|
||||
foreign_keys=[account_id])
|
||||
to_account = db.relationship('Account', back_populates='transfer_transactions',
|
||||
foreign_keys=[to_account_id])
|
||||
category = db.relationship('Category', back_populates='transactions')
|
||||
receipt = db.relationship('Receipt', back_populates='transaction')
|
||||
recurring_rule = db.relationship('RecurringRule', back_populates='transactions')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Transaction {self.transaction_type} {self.amount} on {self.date}>'
|
||||
@@ -0,0 +1,34 @@
|
||||
from flask_login import UserMixin
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app.extensions import db, login_manager
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(64), unique=True, nullable=False, index=True)
|
||||
email = db.Column(db.String(120), unique=True, nullable=True)
|
||||
password_hash = db.Column(db.String(256), nullable=False)
|
||||
display_name = db.Column(db.String(100), nullable=True)
|
||||
timezone = db.Column(db.String(64), default='Asia/Ho_Chi_Minh')
|
||||
currency = db.Column(db.String(10), default='USD')
|
||||
currency_symbol = db.Column(db.String(5), default='$')
|
||||
groq_model = db.Column(db.String(64), default='llama-3.3-70b-versatile')
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
last_login = db.Column(db.DateTime, nullable=True)
|
||||
|
||||
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 __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return db.session.get(User, int(user_id))
|
||||
Reference in New Issue
Block a user