06/05 Optimize app
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user