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, DecimalField from wtforms.validators import DataRequired, Length, Optional, NumberRange 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)]) opening_balance = DecimalField('Opening Balance', places=2, default=0, validators=[Optional()]) 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.flush() # get account.id before creating transaction opening = float(form.opening_balance.data or 0) if opening != 0: from app.models.transaction import Transaction txn_type = 'income' if opening > 0 else 'expense' db.session.add(Transaction( account_id=account.id, transaction_type=txn_type, amount=abs(opening), description='Opening Balance', date=date.today(), notes='Initial account balance', )) db.session.commit() calc_balance(account.id) 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')) @accounts_bp.route('//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)