06/05 Optimize app
This commit is contained in:
+191
-31
@@ -1,4 +1,4 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify, Response, stream_with_context
|
||||
from flask_login import login_required
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SelectField, TextAreaField, SubmitField, DecimalField, DateField, HiddenField
|
||||
@@ -61,39 +61,10 @@ class TransferForm(FlaskForm):
|
||||
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', '')
|
||||
amount_min = request.args.get('amount_min', '')
|
||||
amount_max = request.args.get('amount_max', '')
|
||||
|
||||
from datetime import timedelta
|
||||
today = date.today()
|
||||
this_month_from = today.replace(day=1).strftime('%Y-%m-%d')
|
||||
this_month_to = today.strftime('%Y-%m-%d')
|
||||
last_month_last = today.replace(day=1) - timedelta(days=1)
|
||||
last_month_first = last_month_last.replace(day=1)
|
||||
last_month_from = last_month_first.strftime('%Y-%m-%d')
|
||||
last_month_to = last_month_last.strftime('%Y-%m-%d')
|
||||
|
||||
if date_from == this_month_from and date_to == this_month_to:
|
||||
active_quick = 'this_month'
|
||||
elif date_from == last_month_from and date_to == last_month_to:
|
||||
active_quick = 'last_month'
|
||||
else:
|
||||
active_quick = ''
|
||||
|
||||
def _build_filter_query(tab, search, category_id, account_id, date_from, date_to, amount_min='', amount_max=''):
|
||||
query = Transaction.query.filter(
|
||||
Transaction.transaction_type == tab
|
||||
).order_by(Transaction.date.desc(), Transaction.id.desc())
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
@@ -125,6 +96,39 @@ def index():
|
||||
query = query.filter(Transaction.amount <= float(amount_max))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return query
|
||||
|
||||
|
||||
@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', '')
|
||||
amount_min = request.args.get('amount_min', '')
|
||||
amount_max = request.args.get('amount_max', '')
|
||||
|
||||
from datetime import timedelta
|
||||
today = date.today()
|
||||
this_month_from = today.replace(day=1).strftime('%Y-%m-%d')
|
||||
this_month_to = today.strftime('%Y-%m-%d')
|
||||
last_month_last = today.replace(day=1) - timedelta(days=1)
|
||||
last_month_first = last_month_last.replace(day=1)
|
||||
last_month_from = last_month_first.strftime('%Y-%m-%d')
|
||||
last_month_to = last_month_last.strftime('%Y-%m-%d')
|
||||
|
||||
if date_from == this_month_from and date_to == this_month_to:
|
||||
active_quick = 'this_month'
|
||||
elif date_from == last_month_from and date_to == last_month_to:
|
||||
active_quick = 'last_month'
|
||||
else:
|
||||
active_quick = ''
|
||||
|
||||
query = _build_filter_query(tab, search, category_id, account_id, date_from, date_to, amount_min, amount_max)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=30, error_out=False)
|
||||
|
||||
@@ -483,3 +487,159 @@ def ocr_receipt_file():
|
||||
'category_suggestion': result['category_suggestion'],
|
||||
'category_id': category_id,
|
||||
})
|
||||
|
||||
|
||||
# ── Export filtered transactions ──────────────────────────────────────────────
|
||||
|
||||
@transactions_bp.route('/export/csv')
|
||||
@login_required
|
||||
def export_csv():
|
||||
from app.services.export_service import transactions_csv_stream
|
||||
tab = request.args.get('tab', 'expense')
|
||||
search = request.args.get('q', '').strip()
|
||||
category_id = request.args.get('category_id', '')
|
||||
account_id = request.args.get('account_id', '')
|
||||
date_from = request.args.get('date_from', '')
|
||||
date_to = request.args.get('date_to', '')
|
||||
amount_min = request.args.get('amount_min', '')
|
||||
amount_max = request.args.get('amount_max', '')
|
||||
|
||||
query = _build_filter_query(tab, search, category_id, account_id, date_from, date_to, amount_min, amount_max)
|
||||
filename = f'transactions_{tab}_{date.today().strftime("%Y%m%d")}.csv'
|
||||
return Response(
|
||||
stream_with_context(transactions_csv_stream(query)),
|
||||
mimetype='text/csv',
|
||||
headers={'Content-Disposition': f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
@transactions_bp.route('/export/excel')
|
||||
@login_required
|
||||
def export_excel():
|
||||
from app.services.export_service import transactions_to_excel
|
||||
tab = request.args.get('tab', 'expense')
|
||||
search = request.args.get('q', '').strip()
|
||||
category_id = request.args.get('category_id', '')
|
||||
account_id = request.args.get('account_id', '')
|
||||
date_from = request.args.get('date_from', '')
|
||||
date_to = request.args.get('date_to', '')
|
||||
amount_min = request.args.get('amount_min', '')
|
||||
amount_max = request.args.get('amount_max', '')
|
||||
|
||||
query = _build_filter_query(tab, search, category_id, account_id, date_from, date_to, amount_min, amount_max)
|
||||
label = f'{tab.title()} {date_from or "all"}{"-" + date_to if date_to else ""}'[:31]
|
||||
excel_bytes = transactions_to_excel(query, label)
|
||||
filename = f'transactions_{tab}_{date.today().strftime("%Y%m%d")}.xlsx'
|
||||
return Response(
|
||||
excel_bytes,
|
||||
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
headers={'Content-Disposition': f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
# ── Duplicate detection ───────────────────────────────────────────────────────
|
||||
|
||||
@transactions_bp.route('/api/check-duplicate')
|
||||
@login_required
|
||||
def check_duplicate():
|
||||
txn_date = request.args.get('date', '')
|
||||
amount = request.args.get('amount', '')
|
||||
txn_type = request.args.get('type', 'expense')
|
||||
exclude_id = request.args.get('exclude_id', '', type=str)
|
||||
|
||||
if not txn_date or not amount:
|
||||
return jsonify({'duplicates': []})
|
||||
|
||||
try:
|
||||
d = datetime.strptime(txn_date, '%Y-%m-%d').date()
|
||||
amt = float(amount)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'duplicates': []})
|
||||
|
||||
q = Transaction.query.filter(
|
||||
Transaction.transaction_type == txn_type,
|
||||
Transaction.date == d,
|
||||
Transaction.amount == amt,
|
||||
)
|
||||
if exclude_id:
|
||||
try:
|
||||
q = q.filter(Transaction.id != int(exclude_id))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
dupes = q.limit(5).all()
|
||||
return jsonify({'duplicates': [
|
||||
{'id': t.id, 'description': t.description, 'date': t.date.strftime('%b %d, %Y'),
|
||||
'account': t.account.name if t.account else ''}
|
||||
for t in dupes
|
||||
]})
|
||||
|
||||
|
||||
# ── Split transaction ─────────────────────────────────────────────────────────
|
||||
|
||||
@transactions_bp.route('/<int:id>/split', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def split(id):
|
||||
txn = db.get_or_404(Transaction, id)
|
||||
|
||||
all_cats = Category.query.filter(
|
||||
Category.category_type.in_([txn.transaction_type, 'both']),
|
||||
Category.is_active == True,
|
||||
).order_by(Category.name).all()
|
||||
|
||||
if request.method == 'POST':
|
||||
amounts = request.form.getlist('split_amount')
|
||||
cat_ids = request.form.getlist('split_category')
|
||||
descs = request.form.getlist('split_description')
|
||||
|
||||
parts = []
|
||||
total_split = 0.0
|
||||
for amt_str, cid_str, desc_str in zip(amounts, cat_ids, descs):
|
||||
try:
|
||||
amt = float(amt_str)
|
||||
except (ValueError, TypeError):
|
||||
flash('Invalid amount in split.', 'danger')
|
||||
return redirect(url_for('transactions.split', id=id))
|
||||
if amt <= 0:
|
||||
continue
|
||||
parts.append({
|
||||
'amount': amt,
|
||||
'category_id': int(cid_str) if cid_str else None,
|
||||
'description': desc_str.strip() or txn.description,
|
||||
})
|
||||
total_split += amt
|
||||
|
||||
if not parts:
|
||||
flash('Add at least one split row.', 'danger')
|
||||
return redirect(url_for('transactions.split', id=id))
|
||||
|
||||
if abs(total_split - float(txn.amount)) > 0.005:
|
||||
flash(f'Split total {total_split:.2f} must equal original {float(txn.amount):.2f}.', 'danger')
|
||||
return redirect(url_for('transactions.split', id=id))
|
||||
|
||||
account_id = txn.account_id
|
||||
txn_type = txn.transaction_type
|
||||
txn_date = txn.date
|
||||
txn_notes = txn.notes
|
||||
|
||||
db.session.delete(txn)
|
||||
db.session.flush()
|
||||
|
||||
for part in parts:
|
||||
new_txn = Transaction(
|
||||
transaction_type=txn_type,
|
||||
account_id=account_id,
|
||||
category_id=part['category_id'],
|
||||
amount=part['amount'],
|
||||
description=part['description'],
|
||||
date=txn_date,
|
||||
notes=txn_notes,
|
||||
)
|
||||
db.session.add(new_txn)
|
||||
|
||||
db.session.commit()
|
||||
calc_balance(account_id)
|
||||
flash(f'Transaction split into {len(parts)} part(s).', 'success')
|
||||
return redirect(url_for('transactions.index', tab=txn_type))
|
||||
|
||||
return render_template('transactions/split.html', txn=txn, categories=all_cats)
|
||||
|
||||
Reference in New Issue
Block a user