Commit 4: Added future year forecasting logic
This commit is contained in:
0
backend/backend/backend/backend/hoa.db
Normal file
0
backend/backend/backend/backend/hoa.db
Normal file
0
backend/backend/backend/backend/hoa_app.db
Normal file
0
backend/backend/backend/backend/hoa_app.db
Normal file
@@ -378,9 +378,9 @@ const Forecasting: React.FC = () => {
|
|||||||
smooth: true,
|
smooth: true,
|
||||||
};
|
};
|
||||||
// Add a markLine to show the boundary between actual and projected data
|
// Add a markLine to show the boundary between actual and projected data
|
||||||
// Only add the markLine if we're viewing the current year and have data beyond the current month
|
// Only add the markLine if we're viewing the current year and the current month is within the data range
|
||||||
console.log('Debug markLine:', { year, currentYear, chartDataLength: chartData.length, currentMonth, shouldShow: year === currentYear && chartData.length > currentMonth + 1 });
|
console.log('Debug markLine:', { year, currentYear, chartDataLength: chartData.length, currentMonth, shouldShow: year === currentYear && currentMonth < chartData.length });
|
||||||
const markLine = (year === currentYear && chartData.length > currentMonth + 1) ? {
|
const markLine = (year === currentYear && currentMonth < chartData.length) ? {
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: '#666',
|
color: '#666',
|
||||||
|
|||||||
0
backend/backend/hoa.db
Normal file
0
backend/backend/hoa.db
Normal file
Binary file not shown.
Binary file not shown.
@@ -8,7 +8,7 @@ from fastapi.security import OAuth2PasswordRequestForm
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from .schemas import BucketCreate, BucketRead, AccountCreate, AccountRead, TransactionCreate, TransactionRead, CashFlowCreate, CashFlowRead, ForecastRequest, ForecastResponse, ForecastPoint, ReportRequest, ReportResponse, ReportEntry, CashFlowCategoryCreate, CashFlowCategoryRead, CashFlowEntryCreate, CashFlowEntryRead
|
from .schemas import BucketCreate, BucketRead, AccountCreate, AccountRead, TransactionCreate, TransactionRead, CashFlowCreate, CashFlowRead, ForecastRequest, ForecastResponse, ForecastPoint, ReportRequest, ReportResponse, ReportEntry, CashFlowCategoryCreate, CashFlowCategoryRead, CashFlowEntryCreate, CashFlowEntryRead
|
||||||
from datetime import datetime, timedelta, date as date_cls
|
from datetime import datetime, timedelta, date as date_cls
|
||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func, or_
|
||||||
from .logging_config import setup_logging, DEBUG_LOGGING
|
from .logging_config import setup_logging, DEBUG_LOGGING
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -669,6 +669,8 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
|||||||
|
|
||||||
# --- Cash Flow Projections: Only for Operating bucket ---
|
# --- Cash Flow Projections: Only for Operating bucket ---
|
||||||
cashflow_by_year_month = defaultdict(float)
|
cashflow_by_year_month = defaultdict(float)
|
||||||
|
# --- CD Maturity Tracking: For Reserve bucket ---
|
||||||
|
cd_maturity_by_year_month = defaultdict(float)
|
||||||
if bucket_name.lower() == "operating" and primary_account_id is not None:
|
if bucket_name.lower() == "operating" and primary_account_id is not None:
|
||||||
# Get all cash flow categories for Operating
|
# Get all cash flow categories for Operating
|
||||||
op_categories = db.query(models.CashFlowCategory).filter(models.CashFlowCategory.funding_type == models.CashFlowFundingTypeEnum.operating).all()
|
op_categories = db.query(models.CashFlowCategory).filter(models.CashFlowCategory.funding_type == models.CashFlowFundingTypeEnum.operating).all()
|
||||||
@@ -719,31 +721,46 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
|||||||
cd_funding_tx = None
|
cd_funding_tx = None
|
||||||
# Look for funding transactions in the forecast year first
|
# Look for funding transactions in the forecast year first
|
||||||
for m in range(1, months + 1):
|
for m in range(1, months + 1):
|
||||||
|
# Check both direct deposits and transfers TO this CD
|
||||||
month_txs = txs_by_year_month.get((acc.id, year, m), [])
|
month_txs = txs_by_year_month.get((acc.id, year, m), [])
|
||||||
for tx in month_txs:
|
for tx in month_txs:
|
||||||
if tx.type in ('deposit', 'transfer'):
|
if tx.type in ('deposit', 'transfer'):
|
||||||
cd_funding_tx = tx
|
cd_funding_tx = tx
|
||||||
break
|
break
|
||||||
|
if not cd_funding_tx:
|
||||||
|
# Also check for transfers TO this CD (where related_account_id == acc.id)
|
||||||
|
month_txs = txs_by_year_month.get((acc.id, year, m), [])
|
||||||
|
for tx in month_txs:
|
||||||
|
if tx.type == 'transfer' and tx.related_account_id == acc.id:
|
||||||
|
cd_funding_tx = tx
|
||||||
|
break
|
||||||
if cd_funding_tx:
|
if cd_funding_tx:
|
||||||
funding_month = cd_funding_tx.date.month
|
funding_month = cd_funding_tx.date.month
|
||||||
funding_amount = cd_funding_tx.amount
|
funding_amount = cd_funding_tx.amount
|
||||||
logger.debug(f"CD funding for {acc.name} (id={acc.id}) in month {funding_month}: amount={funding_amount}")
|
logger.debug(f"CD funding for {acc.name} (id={acc.id}) in month {funding_month}: amount={funding_amount}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# If no funding found in forecast year, look for historical funding
|
# If no funding found in forecast year, look for historical funding (including planned transactions)
|
||||||
if not cd_funding_tx:
|
if not cd_funding_tx:
|
||||||
# Get all historical transactions for this CD
|
# Get all transactions for this CD (both reconciled and planned)
|
||||||
|
# Look for both direct deposits and transfers TO this CD
|
||||||
all_cd_txs = db.query(models.Transaction).filter(
|
all_cd_txs = db.query(models.Transaction).filter(
|
||||||
models.Transaction.account_id == acc.id,
|
|
||||||
models.Transaction.type.in_(('deposit', 'transfer')),
|
models.Transaction.type.in_(('deposit', 'transfer')),
|
||||||
models.Transaction.reconciled == True
|
or_(
|
||||||
|
models.Transaction.account_id == acc.id, # Direct deposit to CD
|
||||||
|
models.Transaction.related_account_id == acc.id # Transfer TO CD
|
||||||
|
)
|
||||||
).order_by(models.Transaction.date.asc()).all()
|
).order_by(models.Transaction.date.asc()).all()
|
||||||
|
|
||||||
|
logger.debug(f"CD {acc.name} (id={acc.id}): Found {len(all_cd_txs)} funding transactions")
|
||||||
|
for tx in all_cd_txs:
|
||||||
|
logger.debug(f" - {tx.date}: {tx.amount} ({tx.type}, reconciled={tx.reconciled})")
|
||||||
|
|
||||||
if all_cd_txs:
|
if all_cd_txs:
|
||||||
cd_funding_tx = all_cd_txs[0] # First funding transaction
|
cd_funding_tx = all_cd_txs[0] # First funding transaction
|
||||||
funding_month = cd_funding_tx.date.month
|
funding_month = cd_funding_tx.date.month
|
||||||
funding_amount = cd_funding_tx.amount
|
funding_amount = cd_funding_tx.amount
|
||||||
logger.debug(f"CD funding for {acc.name} (id={acc.id}) found in historical data: month {funding_month}, amount={funding_amount}")
|
logger.debug(f"CD funding for {acc.name} (id={acc.id}) found in historical data: month {funding_month}, amount={funding_amount}, reconciled={cd_funding_tx.reconciled}")
|
||||||
|
|
||||||
# For CDs, determine starting balance based on whether they should exist
|
# For CDs, determine starting balance based on whether they should exist
|
||||||
if cd_funding_tx:
|
if cd_funding_tx:
|
||||||
@@ -751,7 +768,7 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
|||||||
cd_start_age_months = (year - cd_funding_tx.date.year) * 12 + (1 - cd_funding_tx.date.month)
|
cd_start_age_months = (year - cd_funding_tx.date.year) * 12 + (1 - cd_funding_tx.date.month)
|
||||||
|
|
||||||
if cd_start_age_months >= 0:
|
if cd_start_age_months >= 0:
|
||||||
# CD should exist, use previous year's ending balance or validated balance
|
# CD should exist, prioritize previous year's ending balance (which includes projected Dec balance)
|
||||||
if prev_dec_balance is not None:
|
if prev_dec_balance is not None:
|
||||||
balance = prev_dec_balance
|
balance = prev_dec_balance
|
||||||
logger.debug(f"[{acc.name}] CD account, using Dec {year-1} ending balance: {balance}")
|
logger.debug(f"[{acc.name}] CD account, using Dec {year-1} ending balance: {balance}")
|
||||||
@@ -787,6 +804,9 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
|||||||
prev_dec_balance = None
|
prev_dec_balance = None
|
||||||
if (year-1, 12) in monthly_actuals:
|
if (year-1, 12) in monthly_actuals:
|
||||||
prev_dec_balance = monthly_actuals[(year-1, 12)]
|
prev_dec_balance = monthly_actuals[(year-1, 12)]
|
||||||
|
logger.debug(f"[{acc.name}] Found Dec {year-1} balance in monthly_actuals: {prev_dec_balance}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"[{acc.name}] No Dec {year-1} balance found in monthly_actuals. Available keys: {list(monthly_actuals.keys())}")
|
||||||
# Determine starting balance for forecast (skip for CDs since we already set them to $0)
|
# Determine starting balance for forecast (skip for CDs since we already set them to $0)
|
||||||
if not is_cd:
|
if not is_cd:
|
||||||
if earliest_actual_month == 1 and (year, 1) in monthly_actuals:
|
if earliest_actual_month == 1 and (year, 1) in monthly_actuals:
|
||||||
@@ -841,21 +861,13 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
|||||||
if (maturity_date.year == forecast_year and
|
if (maturity_date.year == forecast_year and
|
||||||
maturity_date.month == forecast_month and
|
maturity_date.month == forecast_month and
|
||||||
primary_account_id is not None):
|
primary_account_id is not None):
|
||||||
# Find the CD balance just before maturity
|
# For CD maturity, we need to calculate the balance just before maturity
|
||||||
prev_balance = balance
|
# This will be the balance from the previous month's calculation
|
||||||
if forecast_month > 1:
|
# We'll store this in a variable that gets updated as we process each account
|
||||||
prev_month_date = datetime(forecast_year, forecast_month - 1, 1)
|
matured_amount = balance # This is the CD balance before maturity
|
||||||
prev_key = (forecast_year, forecast_month - 1)
|
if matured_amount > 0:
|
||||||
if prev_key in monthly_actuals:
|
cd_maturity_by_year_month[(forecast_year, forecast_month)] += matured_amount
|
||||||
prev_balance = monthly_actuals[prev_key]
|
logger.debug(f"CD {acc.name} matured: adding ${matured_amount} to CD maturity tracking")
|
||||||
else:
|
|
||||||
# Calculate previous month's balance
|
|
||||||
prev_balance = balance # This will be calculated in the loop
|
|
||||||
|
|
||||||
# Add matured amount to primary account's cash flow
|
|
||||||
if prev_balance > 0:
|
|
||||||
cashflow_by_year_month[(forecast_year, forecast_month)] += prev_balance
|
|
||||||
logger.debug(f"CD {acc.name} matured: adding ${prev_balance} to primary account cash flow")
|
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -900,6 +912,12 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
|||||||
if cf != 0.0:
|
if cf != 0.0:
|
||||||
logger.debug(f"Applying cash flow projection to primary account {acc.name} (id={acc.id}) {forecast_year}-{forecast_month:02d}: {cf}")
|
logger.debug(f"Applying cash flow projection to primary account {acc.name} (id={acc.id}) {forecast_year}-{forecast_month:02d}: {cf}")
|
||||||
bal += cf
|
bal += cf
|
||||||
|
# --- Apply matured CD funds to primary account (Reserve only) ---
|
||||||
|
elif bucket_name.lower() == "reserve" and acc.id == primary_account_id:
|
||||||
|
matured_cd_amount = cd_maturity_by_year_month.get((forecast_year, forecast_month), 0.0)
|
||||||
|
if matured_cd_amount != 0.0:
|
||||||
|
logger.debug(f"Applying matured CD funds to primary account {acc.name} (id={acc.id}) {forecast_year}-{forecast_month:02d}: {matured_cd_amount}")
|
||||||
|
bal += matured_cd_amount
|
||||||
# --- Apply interest if applicable ---
|
# --- Apply interest if applicable ---
|
||||||
if acc.interest_rate and acc.interest_rate > 0:
|
if acc.interest_rate and acc.interest_rate > 0:
|
||||||
interest = bal * (acc.interest_rate / 100.0) / 12.0
|
interest = bal * (acc.interest_rate / 100.0) / 12.0
|
||||||
|
|||||||
Reference in New Issue
Block a user