05/31 Phase 6
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
from flask import (Blueprint, render_template, request, redirect, url_for,
|
||||
Response, flash, send_file)
|
||||
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,
|
||||
)
|
||||
from app.services.export_service import (
|
||||
transactions_to_csv, 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()
|
||||
# Default: current month summary
|
||||
report = monthly_report(today.year, today.month)
|
||||
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,
|
||||
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)
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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)
|
||||
transactions = query.order_by(Transaction.date.desc()).all()
|
||||
|
||||
csv_data = transactions_to_csv(transactions)
|
||||
label = f'{year}-{month:02d}' if month else str(year)
|
||||
filename = f'pfm_transactions_{label}.csv'
|
||||
|
||||
return Response(
|
||||
csv_data,
|
||||
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']),
|
||||
)
|
||||
transactions = query.order_by(Transaction.date.desc()).all()
|
||||
|
||||
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(transactions, 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'))
|
||||
Reference in New Issue
Block a user