Files
Personal-Finance-Management/app/routes/accounts.py
T

196 lines
7.0 KiB
Python

from datetime import date
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length, Optional
from sqlalchemy import func, extract
from app.extensions import db
from app.models.account import Account
from app.models.transaction import Transaction
from app.services.account_service import calc_balance
accounts_bp = Blueprint('accounts', __name__, url_prefix='/accounts')
ACCOUNT_TYPES = [
('checking', 'Checking'),
('savings', 'Savings'),
('cash', 'Cash'),
('credit_card', 'Credit Card'),
('crypto', 'Crypto Wallet'),
('investment', 'Investment'),
('other', 'Other'),
]
ACCOUNT_ICONS = [
('bi-bank', 'Bank'),
('bi-wallet2', 'Wallet'),
('bi-cash-stack', 'Cash'),
('bi-credit-card', 'Credit Card'),
('bi-currency-bitcoin', 'Crypto'),
('bi-graph-up', 'Investment'),
('bi-safe', 'Safe'),
]
ACCOUNT_COLORS = [
'#4F81C7', '#10B981', '#F59E0B', '#EF4444',
'#8B5CF6', '#EC4899', '#06B6D4', '#64748B',
]
class AccountForm(FlaskForm):
name = StringField('Account Name', validators=[DataRequired(), Length(1, 100)])
account_type = SelectField('Type', choices=ACCOUNT_TYPES, validators=[DataRequired()])
color = StringField('Color', default='#4F81C7')
icon = StringField('Icon', default='bi-bank')
notes = TextAreaField('Notes', validators=[Optional(), Length(max=500)])
submit = SubmitField('Save')
BANK_TYPES = {'checking', 'savings', 'cash', 'investment', 'crypto', 'other'}
@accounts_bp.route('/')
@login_required
def index():
tab = request.args.get('tab', 'bank')
if tab not in ('bank', 'credit'):
tab = 'bank'
all_accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
for a in all_accounts:
calc_balance(a.id)
all_accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
bank_accounts = [a for a in all_accounts if a.account_type in BANK_TYPES]
credit_accounts = [a for a in all_accounts if a.account_type == 'credit_card']
accounts = credit_accounts if tab == 'credit' else bank_accounts
# Monthly charges per credit card (current month expenses)
today = date.today()
monthly_charges = {}
if credit_accounts:
rows = db.session.query(
Transaction.account_id,
func.sum(Transaction.amount)
).filter(
Transaction.transaction_type == 'expense',
extract('year', Transaction.date) == today.year,
extract('month', Transaction.date) == today.month,
Transaction.account_id.in_([a.id for a in credit_accounts])
).group_by(Transaction.account_id).all()
monthly_charges = {row[0]: float(row[1]) for row in rows}
return render_template('accounts/index.html',
accounts=accounts,
tab=tab,
bank_count=len(bank_accounts),
credit_count=len(credit_accounts),
monthly_charges=monthly_charges)
@accounts_bp.route('/new', methods=['GET', 'POST'])
@login_required
def new():
form = AccountForm()
if form.validate_on_submit():
account = Account(
name=form.name.data.strip(),
account_type=form.account_type.data,
color=form.color.data or '#4F81C7',
icon=form.icon.data or 'bi-bank',
notes=form.notes.data,
balance=0,
)
db.session.add(account)
db.session.commit()
flash(f'Account "{account.name}" created.', 'success')
return redirect(url_for('accounts.index'))
return render_template('accounts/form.html', form=form, title='New Account',
colors=ACCOUNT_COLORS, icons=ACCOUNT_ICONS)
@accounts_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit(id):
account = db.get_or_404(Account, id)
form = AccountForm(obj=account)
if form.validate_on_submit():
account.name = form.name.data.strip()
account.account_type = form.account_type.data
account.color = form.color.data or account.color
account.icon = form.icon.data or account.icon
account.notes = form.notes.data
db.session.commit()
flash(f'Account "{account.name}" updated.', 'success')
return redirect(url_for('accounts.index'))
return render_template('accounts/form.html', form=form, title='Edit Account',
account=account, colors=ACCOUNT_COLORS, icons=ACCOUNT_ICONS)
@accounts_bp.route('/<int:id>/delete', methods=['POST'])
@login_required
def delete(id):
account = db.get_or_404(Account, id)
# Soft delete
account.is_active = False
db.session.commit()
flash(f'Account "{account.name}" removed.', 'info')
return redirect(url_for('accounts.index'))
@accounts_bp.route('/<int:id>/adjust', methods=['GET', 'POST'])
@login_required
def adjust_balance(id):
account = db.get_or_404(Account, id)
calc_balance(account.id)
db.session.refresh(account)
current_balance = float(account.balance)
if request.method == 'POST':
from app.models.transaction import Transaction
from app.models.category import Category
from wtforms import ValidationError
try:
new_balance = float(request.form.get('new_balance', '').replace(',', ''))
except (ValueError, AttributeError):
flash('Invalid amount entered.', 'danger')
return redirect(url_for('accounts.adjust_balance', id=id))
difference = round(new_balance - current_balance, 2)
if difference == 0:
flash('Balance is already at that amount — no adjustment needed.', 'info')
return redirect(url_for('accounts.index',
tab='credit' if account.account_type == 'credit_card' else 'bank'))
txn_type = 'income' if difference > 0 else 'expense'
amount = abs(difference)
notes = request.form.get('notes', '').strip() or 'Manual balance adjustment'
txn = Transaction(
account_id=account.id,
transaction_type=txn_type,
amount=amount,
description='Balance Adjustment',
date=date.today(),
notes=notes,
)
db.session.add(txn)
db.session.commit()
calc_balance(account.id)
flash(
f'Balance adjusted by '
f'{"+" if difference > 0 else ""}{difference:,.2f}. '
f'New balance: {new_balance:,.2f}.',
'success'
)
return redirect(url_for('accounts.index',
tab='credit' if account.account_type == 'credit_card' else 'bank'))
return render_template('accounts/adjust.html',
account=account,
current_balance=current_balance)