from flask import (Blueprint, render_template, request, redirect, url_for, Response, flash, send_file, stream_with_context) from flask_login import login_required, current_user from app.models.transaction import Transaction from app.services.report_service import ( monthly_report, quarterly_report, yearly_report, net_worth_history, category_trends, tax_year_summary, take_net_worth_snapshot, category_mom_comparison, ) from app.services.export_service import ( transactions_csv_stream, transactions_to_excel, report_to_pdf, build_report_html, ) from datetime import date import io reports_bp = Blueprint('reports', __name__, url_prefix='/reports') _CUR_YEAR = date.today().year _CUR_MONTH = date.today().month @reports_bp.route('/') @login_required def index(): today = date.today() report = monthly_report(today.year, today.month) nw_history = net_worth_history() cat_trend = category_trends(6) mom_data = category_mom_comparison() years = list(range(_CUR_YEAR, _CUR_YEAR - 5, -1)) return render_template('reports/index.html', report=report, nw_history=nw_history, cat_trend=cat_trend, mom_data=mom_data, years=years, current_year=_CUR_YEAR, current_month=_CUR_MONTH, report_type='monthly', selected_year=today.year, selected_month=today.month, selected_quarter=None) @reports_bp.route('/monthly') @login_required def monthly(): year = request.args.get('year', _CUR_YEAR, type=int) month = request.args.get('month', _CUR_MONTH, type=int) report = monthly_report(year, month) nw_history = net_worth_history() cat_trend = category_trends(6) mom_data = category_mom_comparison() years = list(range(_CUR_YEAR, _CUR_YEAR - 5, -1)) return render_template('reports/index.html', report=report, nw_history=nw_history, cat_trend=cat_trend, mom_data=mom_data, years=years, current_year=_CUR_YEAR, current_month=_CUR_MONTH, report_type='monthly', selected_year=year, selected_month=month, selected_quarter=None) @reports_bp.route('/quarterly') @login_required def quarterly(): year = request.args.get('year', _CUR_YEAR, type=int) quarter = request.args.get('quarter', ((_CUR_MONTH - 1) // 3) + 1, type=int) report = quarterly_report(year, quarter) nw_history = net_worth_history() cat_trend = category_trends(6) years = list(range(_CUR_YEAR, _CUR_YEAR - 5, -1)) return render_template('reports/index.html', report=report, nw_history=nw_history, cat_trend=cat_trend, mom_data=[], years=years, current_year=_CUR_YEAR, current_month=_CUR_MONTH, report_type='quarterly', selected_year=year, selected_month=None, selected_quarter=quarter) @reports_bp.route('/yearly') @login_required def yearly(): year = request.args.get('year', _CUR_YEAR, type=int) report = yearly_report(year) nw_history = net_worth_history() cat_trend = category_trends(12) years = list(range(_CUR_YEAR, _CUR_YEAR - 5, -1)) return render_template('reports/index.html', report=report, nw_history=nw_history, cat_trend=cat_trend, mom_data=[], years=years, current_year=_CUR_YEAR, current_month=_CUR_MONTH, report_type='yearly', selected_year=year, selected_month=None, selected_quarter=None) @reports_bp.route('/tax') @login_required def tax(): year = request.args.get('year', _CUR_YEAR, type=int) report = tax_year_summary(year) years = list(range(_CUR_YEAR, _CUR_YEAR - 5, -1)) return render_template('reports/tax.html', report=report, years=years, selected_year=year) # ── Exports ─────────────────────────────────────────────────────────────────── @reports_bp.route('/export/csv') @login_required def export_csv(): year = request.args.get('year', _CUR_YEAR, type=int) month = request.args.get('month', type=int) txn_type = request.args.get('type', 'all') query = Transaction.query.filter( Transaction.date >= date(year, month if month else 1, 1), Transaction.date <= date(year, month if month else 12, 31 if not month else __import__('calendar').monthrange(year, month)[1]), ) if txn_type != 'all': query = query.filter(Transaction.transaction_type == txn_type) query = query.order_by(Transaction.date.desc()) label = f'{year}-{month:02d}' if month else str(year) filename = f'pfm_transactions_{label}.csv' return Response( stream_with_context(transactions_csv_stream(query)), mimetype='text/csv', headers={'Content-Disposition': f'attachment; filename="{filename}"'} ) @reports_bp.route('/export/excel') @login_required def export_excel(): year = request.args.get('year', _CUR_YEAR, type=int) month = request.args.get('month', type=int) query = Transaction.query.filter( Transaction.date >= date(year, month if month else 1, 1), Transaction.date <= date(year, month if month else 12, 31 if not month else __import__('calendar').monthrange(year, month)[1]), Transaction.transaction_type.in_(['income', 'expense']), ) query = query.order_by(Transaction.date.desc()) label = f'{year}-{month:02d}' if month else str(year) period_label = f'{year} Month {month}' if month else str(year) excel_bytes = transactions_to_excel(query, period_label) filename = f'pfm_transactions_{label}.xlsx' return send_file( io.BytesIO(excel_bytes), mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=filename, ) @reports_bp.route('/export/pdf') @login_required def export_pdf(): report_type = request.args.get('type', 'monthly') year = request.args.get('year', _CUR_YEAR, type=int) month = request.args.get('month', _CUR_MONTH, type=int) quarter = request.args.get('quarter', 1, type=int) if report_type == 'yearly': report = yearly_report(year) elif report_type == 'quarterly': report = quarterly_report(year, quarter) else: report = monthly_report(year, month) symbol = current_user.currency_symbol or '$' html = build_report_html(report, report_type, symbol) pdf = report_to_pdf(html) if pdf is None: flash('PDF generation failed. WeasyPrint may not be installed.', 'danger') return redirect(url_for('reports.index')) filename = f'pfm_report_{report["period"].replace(" ", "_")}.pdf' return send_file( io.BytesIO(pdf), mimetype='application/pdf', as_attachment=True, download_name=filename, ) @reports_bp.route('/snapshot', methods=['POST']) @login_required def manual_snapshot(): """Manually trigger a net worth snapshot.""" snap = take_net_worth_snapshot() flash(f'Net worth snapshot saved: {snap.snapshot_date.strftime("%B %d, %Y")}', 'success') return redirect(url_for('reports.index')) @reports_bp.route('/rebuild-snapshot', methods=['POST']) @login_required def rebuild_snapshot(): """Recalculate all balances then replace today's snapshot with fresh data.""" from app.services.account_service import recalc_all from app.models.net_worth_snapshot import NetWorthSnapshot recalc_all() # Delete today's snapshot so take_net_worth_snapshot() creates a fresh one today = date.today() existing = NetWorthSnapshot.query.filter_by(snapshot_date=today).first() if existing: from app.extensions import db db.session.delete(existing) db.session.commit() snap = take_net_worth_snapshot() flash( f'Balances recalculated and snapshot rebuilt for {snap.snapshot_date.strftime("%B %d, %Y")}.', 'success' ) return redirect(url_for('reports.index'))