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('//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('//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'))