06/05 Optimize app

This commit is contained in:
2026-06-05 15:23:23 -04:00
parent db44d6057b
commit 9c9aa694c4
9 changed files with 781 additions and 19 deletions
+98
View File
@@ -94,6 +94,104 @@ def process_due_rules(dry_run=False):
return created
def projected_cash_flow(days=90):
"""
Build a projected cash flow from all active recurring rules over the next
N days. Returns weekly-bucketed chart data plus a flat event list.
Returns dict:
labels — list of 'Mon DD' strings (week-start dates)
income — list of floats (income per week bucket)
expense — list of floats (expense per week bucket)
balance — list of floats (running balance at end of each bucket)
events — list of {date, description, amount, type, rule_id}
starting_balance — float
ending_balance — float
total_income — float
total_expense — float
net — float
"""
from app.models.account import Account
from sqlalchemy import func
from app.extensions import db
today = date.today()
cutoff = today + timedelta(days=days)
# Starting balance = sum of all active account balances
starting_balance = float(
db.session.query(func.coalesce(func.sum(Account.balance), 0))
.filter(Account.is_active == True)
.scalar()
)
# Enumerate all occurrences of active rules within the window
rules = RecurringRule.query.filter_by(is_active=True).all()
events = []
for rule in rules:
run_date = rule.next_run or rule.start_date
# Advance to window start if rule fires before today
while run_date < today:
run_date = next_occurrence(run_date, rule.frequency)
while run_date <= cutoff:
if rule.end_date and run_date > rule.end_date:
break
events.append({
'date': run_date,
'description': rule.description,
'amount': float(rule.amount),
'type': rule.transaction_type,
'rule_id': rule.id,
})
run_date = next_occurrence(run_date, rule.frequency)
events.sort(key=lambda e: e['date'])
# Build weekly buckets: each bucket starts on Monday
# Find the Monday on or before today
week_start = today - timedelta(days=today.weekday())
buckets = []
ws = week_start
while ws <= cutoff:
buckets.append(ws)
ws += timedelta(weeks=1)
bucket_income = [0.0] * len(buckets)
bucket_expense = [0.0] * len(buckets)
for ev in events:
# Find which bucket this event falls in
idx = (ev['date'] - week_start).days // 7
if 0 <= idx < len(buckets):
if ev['type'] == 'income':
bucket_income[idx] += ev['amount']
else:
bucket_expense[idx] += ev['amount']
# Running balance
running = starting_balance
bucket_balance = []
for inc, exp in zip(bucket_income, bucket_expense):
running += inc - exp
bucket_balance.append(round(running, 2))
total_income = sum(bucket_income)
total_expense = sum(bucket_expense)
return {
'labels': [b.strftime('%b %d') for b in buckets],
'income': [round(v, 2) for v in bucket_income],
'expense': [round(v, 2) for v in bucket_expense],
'balance': bucket_balance,
'events': events,
'starting_balance': round(starting_balance, 2),
'ending_balance': round(bucket_balance[-1], 2) if bucket_balance else round(starting_balance, 2),
'total_income': round(total_income, 2),
'total_expense': round(total_expense, 2),
'net': round(total_income - total_expense, 2),
}
def get_upcoming(days=30):
"""Return list of upcoming recurring transactions in the next N days."""
today = date.today()