05/31 Phase 2
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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 app.extensions import db
|
||||
from app.models.account import Account
|
||||
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')
|
||||
|
||||
|
||||
@accounts_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
|
||||
# Recalc balances on page load
|
||||
for a in accounts:
|
||||
calc_balance(a.id)
|
||||
accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
|
||||
return render_template('accounts/index.html', accounts=accounts)
|
||||
|
||||
|
||||
@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'))
|
||||
@@ -0,0 +1,119 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SelectField, SubmitField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
from app.extensions import db
|
||||
from app.models.category import Category
|
||||
|
||||
categories_bp = Blueprint('categories', __name__, url_prefix='/categories')
|
||||
|
||||
CATEGORY_TYPES = [
|
||||
('expense', 'Expense'),
|
||||
('income', 'Income'),
|
||||
('both', 'Both'),
|
||||
]
|
||||
|
||||
CATEGORY_COLORS = [
|
||||
'#6366f1', '#f59e0b', '#3b82f6', '#8b5cf6', '#ef4444',
|
||||
'#ec4899', '#f97316', '#14b8a6', '#64748b', '#10b981',
|
||||
'#0ea5e9', '#84cc16', '#a78bfa', '#94a3b8', '#f43f5e',
|
||||
]
|
||||
|
||||
CATEGORY_ICONS = [
|
||||
'bi-house', 'bi-cup-hot', 'bi-car-front', 'bi-lightning-charge',
|
||||
'bi-heart-pulse', 'bi-controller', 'bi-bag', 'bi-book',
|
||||
'bi-shield-check', 'bi-person-heart', 'bi-airplane', 'bi-repeat',
|
||||
'bi-gift', 'bi-three-dots', 'bi-briefcase', 'bi-laptop',
|
||||
'bi-building', 'bi-graph-up-arrow', 'bi-house-door', 'bi-tag',
|
||||
'bi-cart', 'bi-music-note', 'bi-phone', 'bi-tools',
|
||||
]
|
||||
|
||||
|
||||
class CategoryForm(FlaskForm):
|
||||
name = StringField('Name', validators=[DataRequired(), Length(1, 100)])
|
||||
category_type = SelectField('Type', choices=CATEGORY_TYPES, validators=[DataRequired()])
|
||||
color = StringField('Color', default='#6B7280')
|
||||
icon = StringField('Icon', default='bi-tag')
|
||||
submit = SubmitField('Save')
|
||||
|
||||
|
||||
@categories_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
expense_cats = Category.query.filter(
|
||||
Category.category_type.in_(['expense', 'both']),
|
||||
Category.is_active == True,
|
||||
Category.parent_id == None
|
||||
).order_by(Category.is_system.desc(), Category.name).all()
|
||||
|
||||
income_cats = Category.query.filter(
|
||||
Category.category_type.in_(['income', 'both']),
|
||||
Category.is_active == True,
|
||||
Category.parent_id == None
|
||||
).order_by(Category.is_system.desc(), Category.name).all()
|
||||
|
||||
return render_template('categories/index.html',
|
||||
expense_cats=expense_cats,
|
||||
income_cats=income_cats)
|
||||
|
||||
|
||||
@categories_bp.route('/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new():
|
||||
form = CategoryForm()
|
||||
# Pre-select type from query param
|
||||
if request.method == 'GET' and request.args.get('type'):
|
||||
form.category_type.data = request.args.get('type')
|
||||
|
||||
if form.validate_on_submit():
|
||||
cat = Category(
|
||||
name=form.name.data.strip(),
|
||||
category_type=form.category_type.data,
|
||||
color=form.color.data or '#6B7280',
|
||||
icon=form.icon.data or 'bi-tag',
|
||||
is_system=False,
|
||||
)
|
||||
db.session.add(cat)
|
||||
db.session.commit()
|
||||
flash(f'Category "{cat.name}" created.', 'success')
|
||||
return redirect(url_for('categories.index'))
|
||||
return render_template('categories/form.html', form=form, title='New Category',
|
||||
colors=CATEGORY_COLORS, icons=CATEGORY_ICONS)
|
||||
|
||||
|
||||
@categories_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit(id):
|
||||
cat = db.get_or_404(Category, id)
|
||||
form = CategoryForm(obj=cat)
|
||||
if form.validate_on_submit():
|
||||
if cat.is_system and cat.name != form.name.data.strip():
|
||||
flash('System category names cannot be changed.', 'warning')
|
||||
else:
|
||||
cat.name = form.name.data.strip()
|
||||
cat.category_type = form.category_type.data
|
||||
cat.color = form.color.data or cat.color
|
||||
cat.icon = form.icon.data or cat.icon
|
||||
db.session.commit()
|
||||
flash(f'Category "{cat.name}" updated.', 'success')
|
||||
return redirect(url_for('categories.index'))
|
||||
return render_template('categories/form.html', form=form, title='Edit Category',
|
||||
category=cat, colors=CATEGORY_COLORS, icons=CATEGORY_ICONS)
|
||||
|
||||
|
||||
@categories_bp.route('/<int:id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete(id):
|
||||
cat = db.get_or_404(Category, id)
|
||||
if cat.is_system:
|
||||
flash('System categories cannot be deleted.', 'warning')
|
||||
return redirect(url_for('categories.index'))
|
||||
# Check if in use
|
||||
if cat.transactions.count() > 0:
|
||||
flash('Cannot delete category with existing transactions. Deactivate instead.', 'warning')
|
||||
return redirect(url_for('categories.index'))
|
||||
cat.is_active = False
|
||||
db.session.commit()
|
||||
flash(f'Category "{cat.name}" removed.', 'info')
|
||||
return redirect(url_for('categories.index'))
|
||||
+151
-3
@@ -1,10 +1,158 @@
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy import func, extract
|
||||
from app.extensions import db
|
||||
from app.models.account import Account
|
||||
from app.models.transaction import Transaction
|
||||
from app.models.category import Category
|
||||
from app.services.fx_service import get_today_rate, get_rate_history
|
||||
from app.services.account_service import get_total_assets, get_total_liabilities
|
||||
from datetime import date, datetime, timedelta
|
||||
import calendar
|
||||
|
||||
dashboard_bp = Blueprint('dashboard', __name__)
|
||||
|
||||
|
||||
def _parse_date_range(period):
|
||||
"""Return (date_from, date_to, label) for a given period string."""
|
||||
today = date.today()
|
||||
if period == 'last_month':
|
||||
first = (today.replace(day=1) - timedelta(days=1)).replace(day=1)
|
||||
last = today.replace(day=1) - timedelta(days=1)
|
||||
label = first.strftime('%B %Y')
|
||||
elif period == 'custom':
|
||||
try:
|
||||
date_from = datetime.strptime(request.args.get('date_from', ''), '%Y-%m-%d').date()
|
||||
date_to = datetime.strptime(request.args.get('date_to', ''), '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
date_from = today.replace(day=1)
|
||||
date_to = today
|
||||
return date_from, date_to, 'Custom Range'
|
||||
else: # this_month (default)
|
||||
first = today.replace(day=1)
|
||||
last = today
|
||||
label = first.strftime('%B %Y')
|
||||
return first, last, label
|
||||
|
||||
|
||||
@dashboard_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
return render_template('dashboard/index.html')
|
||||
period = request.args.get('period', 'this_month')
|
||||
date_from, date_to, period_label = _parse_date_range(period)
|
||||
|
||||
# ── Summary cards ────────────────────────────────
|
||||
total_income = db.session.query(
|
||||
func.coalesce(func.sum(Transaction.amount), 0)
|
||||
).filter(
|
||||
Transaction.transaction_type == 'income',
|
||||
Transaction.date >= date_from,
|
||||
Transaction.date <= date_to,
|
||||
).scalar()
|
||||
|
||||
total_expense = db.session.query(
|
||||
func.coalesce(func.sum(Transaction.amount), 0)
|
||||
).filter(
|
||||
Transaction.transaction_type == 'expense',
|
||||
Transaction.date >= date_from,
|
||||
Transaction.date <= date_to,
|
||||
).scalar()
|
||||
|
||||
net_cash_flow = float(total_income) - float(total_expense)
|
||||
|
||||
# ── Accounts ─────────────────────────────────────
|
||||
accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
|
||||
total_assets = get_total_assets()
|
||||
total_liabilities = get_total_liabilities()
|
||||
net_worth = total_assets - total_liabilities
|
||||
|
||||
# ── Top expense categories ────────────────────────
|
||||
top_categories = db.session.query(
|
||||
Category.name,
|
||||
Category.color,
|
||||
Category.icon,
|
||||
func.sum(Transaction.amount).label('total')
|
||||
).join(Transaction, Transaction.category_id == Category.id)\
|
||||
.filter(
|
||||
Transaction.transaction_type == 'expense',
|
||||
Transaction.date >= date_from,
|
||||
Transaction.date <= date_to,
|
||||
).group_by(Category.id)\
|
||||
.order_by(func.sum(Transaction.amount).desc())\
|
||||
.limit(5).all()
|
||||
|
||||
# ── Cash flow chart (last 6 months) ──────────────
|
||||
chart_months = []
|
||||
chart_income = []
|
||||
chart_expense = []
|
||||
today = date.today()
|
||||
for i in range(5, -1, -1):
|
||||
# go back i months from current
|
||||
month_date = (today.replace(day=1) - timedelta(days=i * 28)).replace(day=1)
|
||||
last_day = calendar.monthrange(month_date.year, month_date.month)[1]
|
||||
m_start = month_date
|
||||
m_end = month_date.replace(day=last_day)
|
||||
|
||||
inc = db.session.query(
|
||||
func.coalesce(func.sum(Transaction.amount), 0)
|
||||
).filter(
|
||||
Transaction.transaction_type == 'income',
|
||||
Transaction.date >= m_start,
|
||||
Transaction.date <= m_end,
|
||||
).scalar()
|
||||
|
||||
exp = db.session.query(
|
||||
func.coalesce(func.sum(Transaction.amount), 0)
|
||||
).filter(
|
||||
Transaction.transaction_type == 'expense',
|
||||
Transaction.date >= m_start,
|
||||
Transaction.date <= m_end,
|
||||
).scalar()
|
||||
|
||||
chart_months.append(month_date.strftime('%b %Y'))
|
||||
chart_income.append(float(inc))
|
||||
chart_expense.append(float(exp))
|
||||
|
||||
# ── Recent transactions ───────────────────────────
|
||||
recent_txns = Transaction.query\
|
||||
.filter(Transaction.transaction_type.in_(['income', 'expense']))\
|
||||
.order_by(Transaction.date.desc(), Transaction.id.desc())\
|
||||
.limit(8).all()
|
||||
|
||||
# ── USD/VND rate ──────────────────────────────────
|
||||
fx = get_today_rate()
|
||||
fx_history = get_rate_history(30)
|
||||
fx_history_data = {
|
||||
'dates': [r.date.strftime('%b %d') for r in fx_history],
|
||||
'rates': [float(r.usd_to_vnd) for r in fx_history],
|
||||
}
|
||||
|
||||
return render_template('dashboard/index.html',
|
||||
period=period,
|
||||
period_label=period_label,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
total_income=float(total_income),
|
||||
total_expense=float(total_expense),
|
||||
net_cash_flow=net_cash_flow,
|
||||
accounts=accounts,
|
||||
total_assets=total_assets,
|
||||
total_liabilities=total_liabilities,
|
||||
net_worth=net_worth,
|
||||
top_categories=top_categories,
|
||||
chart_months=chart_months,
|
||||
chart_income=chart_income,
|
||||
chart_expense=chart_expense,
|
||||
recent_txns=recent_txns,
|
||||
fx=fx,
|
||||
fx_history_data=fx_history_data)
|
||||
|
||||
|
||||
@dashboard_bp.route('/api/fx-history')
|
||||
@login_required
|
||||
def fx_history_api():
|
||||
history = get_rate_history(30)
|
||||
return jsonify({
|
||||
'dates': [r.date.strftime('%b %d') for r in history],
|
||||
'rates': [float(r.usd_to_vnd) for r in history],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
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, DateField, HiddenField
|
||||
from wtforms.validators import DataRequired, Optional, NumberRange
|
||||
from app.extensions import db
|
||||
from app.models.transaction import Transaction
|
||||
from app.models.account import Account
|
||||
from app.models.category import Category
|
||||
from app.services.account_service import calc_balance
|
||||
from datetime import date, datetime
|
||||
from sqlalchemy import or_
|
||||
|
||||
transactions_bp = Blueprint('transactions', __name__, url_prefix='/transactions')
|
||||
|
||||
|
||||
def _account_choices():
|
||||
return [(str(a.id), a.name)
|
||||
for a in Account.query.filter_by(is_active=True).order_by(Account.name).all()]
|
||||
|
||||
|
||||
def _category_choices(cat_type):
|
||||
cats = Category.query.filter(
|
||||
Category.category_type.in_([cat_type, 'both']),
|
||||
Category.is_active == True,
|
||||
Category.parent_id == None
|
||||
).order_by(Category.name).all()
|
||||
return [('', '— None —')] + [(str(c.id), c.name) for c in cats]
|
||||
|
||||
|
||||
class TransactionForm(FlaskForm):
|
||||
transaction_type = HiddenField(default='expense')
|
||||
account_id = SelectField('Account', validators=[DataRequired()])
|
||||
category_id = SelectField('Category', validators=[Optional()])
|
||||
amount = DecimalField('Amount', validators=[DataRequired(), NumberRange(min=0.01)],
|
||||
places=2)
|
||||
description = StringField('Description', validators=[DataRequired()])
|
||||
date = DateField('Date', validators=[DataRequired()], default=date.today)
|
||||
notes = TextAreaField('Notes', validators=[Optional()])
|
||||
submit = SubmitField('Save')
|
||||
|
||||
|
||||
class TransferForm(FlaskForm):
|
||||
from_account_id = SelectField('From Account', validators=[DataRequired()])
|
||||
to_account_id = SelectField('To Account', validators=[DataRequired()])
|
||||
amount = DecimalField('Amount', validators=[DataRequired(), NumberRange(min=0.01)],
|
||||
places=2)
|
||||
description = StringField('Description', default='Transfer')
|
||||
date = DateField('Date', validators=[DataRequired()], default=date.today)
|
||||
notes = TextAreaField('Notes', validators=[Optional()])
|
||||
submit = SubmitField('Transfer')
|
||||
|
||||
|
||||
@transactions_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
tab = request.args.get('tab', 'expense') # 'income' | 'expense'
|
||||
page = request.args.get('page', 1, type=int)
|
||||
search = request.args.get('q', '').strip()
|
||||
category_id = request.args.get('category_id', '', type=str)
|
||||
account_id = request.args.get('account_id', '', type=str)
|
||||
date_from = request.args.get('date_from', '')
|
||||
date_to = request.args.get('date_to', '')
|
||||
|
||||
query = Transaction.query.filter(
|
||||
Transaction.transaction_type == tab
|
||||
).order_by(Transaction.date.desc(), Transaction.id.desc())
|
||||
|
||||
if search:
|
||||
query = query.filter(Transaction.description.ilike(f'%{search}%'))
|
||||
if category_id:
|
||||
query = query.filter(Transaction.category_id == int(category_id))
|
||||
if account_id:
|
||||
query = query.filter(Transaction.account_id == int(account_id))
|
||||
if date_from:
|
||||
try:
|
||||
query = query.filter(Transaction.date >= datetime.strptime(date_from, '%Y-%m-%d').date())
|
||||
except ValueError:
|
||||
pass
|
||||
if date_to:
|
||||
try:
|
||||
query = query.filter(Transaction.date <= datetime.strptime(date_to, '%Y-%m-%d').date())
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
pagination = query.paginate(page=page, per_page=30, error_out=False)
|
||||
|
||||
# Counts for tabs
|
||||
income_count = Transaction.query.filter_by(transaction_type='income').count()
|
||||
expense_count = Transaction.query.filter_by(transaction_type='expense').count()
|
||||
|
||||
accounts = Account.query.filter_by(is_active=True).order_by(Account.name).all()
|
||||
categories = Category.query.filter(
|
||||
Category.category_type.in_([tab, 'both']),
|
||||
Category.is_active == True
|
||||
).order_by(Category.name).all()
|
||||
|
||||
return render_template('transactions/index.html',
|
||||
pagination=pagination,
|
||||
transactions=pagination.items,
|
||||
tab=tab,
|
||||
income_count=income_count,
|
||||
expense_count=expense_count,
|
||||
accounts=accounts,
|
||||
categories=categories,
|
||||
search=search,
|
||||
category_id=category_id,
|
||||
account_id=account_id,
|
||||
date_from=date_from,
|
||||
date_to=date_to)
|
||||
|
||||
|
||||
@transactions_bp.route('/new', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def new():
|
||||
txn_type = request.args.get('type', 'expense')
|
||||
if txn_type not in ('income', 'expense'):
|
||||
txn_type = 'expense'
|
||||
|
||||
form = TransactionForm()
|
||||
form.transaction_type.data = txn_type
|
||||
form.account_id.choices = _account_choices()
|
||||
form.category_id.choices = _category_choices(txn_type)
|
||||
|
||||
if form.validate_on_submit():
|
||||
txn = Transaction(
|
||||
transaction_type=form.transaction_type.data,
|
||||
account_id=int(form.account_id.data),
|
||||
category_id=int(form.category_id.data) if form.category_id.data else None,
|
||||
amount=form.amount.data,
|
||||
description=form.description.data.strip(),
|
||||
date=form.date.data,
|
||||
notes=form.notes.data,
|
||||
)
|
||||
db.session.add(txn)
|
||||
db.session.commit()
|
||||
calc_balance(txn.account_id)
|
||||
flash(f'{"Income" if txn_type == "income" else "Expense"} added.', 'success')
|
||||
return redirect(url_for('transactions.index', tab=txn_type))
|
||||
|
||||
return render_template('transactions/form.html',
|
||||
form=form,
|
||||
txn_type=txn_type,
|
||||
title=f'New {"Income" if txn_type == "income" else "Expense"}')
|
||||
|
||||
|
||||
@transactions_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit(id):
|
||||
txn = db.get_or_404(Transaction, id)
|
||||
form = TransactionForm(obj=txn)
|
||||
form.transaction_type.data = txn.transaction_type
|
||||
form.account_id.choices = _account_choices()
|
||||
form.category_id.choices = _category_choices(txn.transaction_type)
|
||||
|
||||
# Pre-populate foreign keys as strings for SelectField
|
||||
if request.method == 'GET':
|
||||
form.account_id.data = str(txn.account_id)
|
||||
form.category_id.data = str(txn.category_id) if txn.category_id else ''
|
||||
|
||||
if form.validate_on_submit():
|
||||
old_account_id = txn.account_id
|
||||
txn.account_id = int(form.account_id.data)
|
||||
txn.category_id = int(form.category_id.data) if form.category_id.data else None
|
||||
txn.amount = form.amount.data
|
||||
txn.description = form.description.data.strip()
|
||||
txn.date = form.date.data
|
||||
txn.notes = form.notes.data
|
||||
db.session.commit()
|
||||
calc_balance(old_account_id)
|
||||
calc_balance(txn.account_id)
|
||||
flash('Transaction updated.', 'success')
|
||||
return redirect(url_for('transactions.index', tab=txn.transaction_type))
|
||||
|
||||
return render_template('transactions/form.html',
|
||||
form=form,
|
||||
txn=txn,
|
||||
txn_type=txn.transaction_type,
|
||||
title='Edit Transaction')
|
||||
|
||||
|
||||
@transactions_bp.route('/<int:id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete(id):
|
||||
txn = db.get_or_404(Transaction, id)
|
||||
account_id = txn.account_id
|
||||
txn_type = txn.transaction_type
|
||||
db.session.delete(txn)
|
||||
db.session.commit()
|
||||
calc_balance(account_id)
|
||||
flash('Transaction deleted.', 'info')
|
||||
return redirect(url_for('transactions.index', tab=txn_type))
|
||||
|
||||
|
||||
@transactions_bp.route('/transfer', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def transfer():
|
||||
form = TransferForm()
|
||||
form.from_account_id.choices = _account_choices()
|
||||
form.to_account_id.choices = _account_choices()
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.from_account_id.data == form.to_account_id.data:
|
||||
flash('Source and destination accounts must be different.', 'warning')
|
||||
else:
|
||||
txn = Transaction(
|
||||
transaction_type='transfer',
|
||||
account_id=int(form.from_account_id.data),
|
||||
to_account_id=int(form.to_account_id.data),
|
||||
amount=form.amount.data,
|
||||
description=form.description.data.strip() or 'Transfer',
|
||||
date=form.date.data,
|
||||
notes=form.notes.data,
|
||||
)
|
||||
db.session.add(txn)
|
||||
db.session.commit()
|
||||
calc_balance(txn.account_id)
|
||||
calc_balance(txn.to_account_id)
|
||||
flash('Transfer recorded.', 'success')
|
||||
return redirect(url_for('transactions.index'))
|
||||
|
||||
return render_template('transactions/transfer.html', form=form)
|
||||
Reference in New Issue
Block a user