Fixes for logo and Forecasting
This commit is contained in:
@@ -1 +1,16 @@
|
||||
<svg width="200" height="50" viewBox="0 0 200 50" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Light blue circle background -->
|
||||
<circle cx="25" cy="25" r="20" fill="#87CEEB"/>
|
||||
|
||||
<!-- White upward arrow -->
|
||||
<path d="M15 30 L25 15 L35 30 L30 30 L30 35 L20 35 L20 30 Z" fill="white"/>
|
||||
|
||||
<!-- White bar chart -->
|
||||
<rect x="32" y="25" width="3" height="8" fill="white"/>
|
||||
<rect x="37" y="20" width="3" height="13" fill="white"/>
|
||||
|
||||
<!-- HOApro text -->
|
||||
<text x="55" y="32" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#1E3A8A">
|
||||
HOApro
|
||||
</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1 B After Width: | Height: | Size: 592 B |
@@ -320,8 +320,8 @@ const Forecasting: React.FC = () => {
|
||||
// Create data with different itemStyle for actual vs projected
|
||||
const styledData = chartData.map((row, dataIdx) => {
|
||||
const rowDate = new Date(row.rawDate);
|
||||
const isProjected = rowDate.getFullYear() > 2025 ||
|
||||
(rowDate.getFullYear() === 2025 && rowDate.getMonth() >= 7); // July and later
|
||||
const isProjected = rowDate.getFullYear() > currentYear ||
|
||||
(rowDate.getFullYear() === currentYear && rowDate.getMonth() > currentMonth);
|
||||
|
||||
return {
|
||||
value: row[key],
|
||||
@@ -353,8 +353,8 @@ const Forecasting: React.FC = () => {
|
||||
// Add total as a line with actual vs projected styling
|
||||
const totalStyledData = chartData.map((row, dataIdx) => {
|
||||
const rowDate = new Date(row.rawDate);
|
||||
const isProjected = rowDate.getFullYear() > 2025 ||
|
||||
(rowDate.getFullYear() === 2025 && rowDate.getMonth() >= 7); // July and later
|
||||
const isProjected = rowDate.getFullYear() > currentYear ||
|
||||
(rowDate.getFullYear() === currentYear && rowDate.getMonth() > currentMonth);
|
||||
|
||||
return {
|
||||
value: row.total,
|
||||
@@ -378,12 +378,9 @@ const Forecasting: React.FC = () => {
|
||||
smooth: true,
|
||||
};
|
||||
// Add a markLine to show the boundary between actual and projected data
|
||||
// Position the line between July and August (month 6 and 7, 0-based)
|
||||
const julyIndex = 6; // July is month 7, but 0-based index is 6
|
||||
const augustIndex = 7; // August is month 8, but 0-based index is 7
|
||||
|
||||
// Only add the markLine if we're viewing 2025 and have data for August
|
||||
const markLine = (year === 2025 && chartData.length > augustIndex) ? {
|
||||
// Only add the markLine if we're viewing the current year and have data beyond the current month
|
||||
console.log('Debug markLine:', { year, currentYear, chartDataLength: chartData.length, currentMonth, shouldShow: year === currentYear && chartData.length > currentMonth + 1 });
|
||||
const markLine = (year === currentYear && chartData.length > currentMonth + 1) ? {
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#666',
|
||||
@@ -398,7 +395,7 @@ const Forecasting: React.FC = () => {
|
||||
color: '#666'
|
||||
},
|
||||
data: [
|
||||
{ xAxis: julyIndex + 0.5 } // Position between July and August
|
||||
{ xAxis: currentMonth + 0.5 } // Position between current month and next month
|
||||
]
|
||||
} : null;
|
||||
|
||||
|
||||
Binary file not shown.
@@ -660,6 +660,7 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
||||
primary_account = acc
|
||||
break
|
||||
alert_threshold = 1000.0
|
||||
|
||||
# 2. Get historical balances for each account
|
||||
history_by_account = defaultdict(list)
|
||||
histories = db.query(models.AccountBalanceHistory).filter(models.AccountBalanceHistory.account_id.in_(account_ids)).order_by(models.AccountBalanceHistory.date.asc()).all()
|
||||
@@ -707,13 +708,16 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
||||
earliest_actual_month = None
|
||||
if monthly_actuals:
|
||||
earliest_actual_month = min([m for (y, m) in monthly_actuals.keys() if y == year], default=None)
|
||||
|
||||
# --- CD-specific logic (must happen before balance determination) ---
|
||||
is_cd = hasattr(acc, 'account_type') and acc.account_type and acc.account_type.name.lower() == 'cd'
|
||||
funding_month = None
|
||||
funding_amount = 0.0
|
||||
|
||||
if is_cd:
|
||||
# Find first reconciled deposit/transfer into this CD in the forecast year
|
||||
# Find first reconciled deposit/transfer into this CD in ANY year (not just forecast year)
|
||||
cd_funding_tx = None
|
||||
# Look for funding transactions in the forecast year first
|
||||
for m in range(1, months + 1):
|
||||
month_txs = txs_by_year_month.get((acc.id, year, m), [])
|
||||
for tx in month_txs:
|
||||
@@ -725,10 +729,52 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
||||
funding_amount = cd_funding_tx.amount
|
||||
logger.debug(f"CD funding for {acc.name} (id={acc.id}) in month {funding_month}: amount={funding_amount}")
|
||||
break
|
||||
# For CDs, always start with $0 balance - they don't exist before funding
|
||||
balance = 0.0
|
||||
logger.debug(f"[{acc.name}] CD account, using $0 starting balance (will be funded in month {funding_month if funding_month else 'unknown'})")
|
||||
|
||||
# If no funding found in forecast year, look for historical funding
|
||||
if not cd_funding_tx:
|
||||
# Get all historical transactions for this CD
|
||||
all_cd_txs = db.query(models.Transaction).filter(
|
||||
models.Transaction.account_id == acc.id,
|
||||
models.Transaction.type.in_(('deposit', 'transfer')),
|
||||
models.Transaction.reconciled == True
|
||||
).order_by(models.Transaction.date.asc()).all()
|
||||
|
||||
if all_cd_txs:
|
||||
cd_funding_tx = all_cd_txs[0] # First funding transaction
|
||||
funding_month = cd_funding_tx.date.month
|
||||
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}")
|
||||
|
||||
# For CDs, determine starting balance based on whether they should exist
|
||||
if cd_funding_tx:
|
||||
# Calculate if CD should exist at the start of the forecast year
|
||||
cd_start_age_months = (year - cd_funding_tx.date.year) * 12 + (1 - cd_funding_tx.date.month)
|
||||
|
||||
if cd_start_age_months >= 0:
|
||||
# CD should exist, use previous year's ending balance or validated balance
|
||||
if prev_dec_balance is not None:
|
||||
balance = prev_dec_balance
|
||||
logger.debug(f"[{acc.name}] CD account, using Dec {year-1} ending balance: {balance}")
|
||||
elif validated_balances[acc.id] > 0:
|
||||
balance = validated_balances[acc.id]
|
||||
logger.debug(f"[{acc.name}] CD account, using validated balance: {balance}")
|
||||
else:
|
||||
# Calculate balance from funding amount and interest
|
||||
balance = funding_amount
|
||||
# Apply interest for months since funding
|
||||
if acc.interest_rate and acc.interest_rate > 0:
|
||||
for month_offset in range(cd_start_age_months):
|
||||
interest = balance * (acc.interest_rate / 100.0) / 12.0
|
||||
balance += interest
|
||||
logger.debug(f"[{acc.name}] CD account, calculated balance from funding: {balance}")
|
||||
else:
|
||||
# CD hasn't been funded yet
|
||||
balance = 0.0
|
||||
logger.debug(f"[{acc.name}] CD account, not funded yet, using $0 starting balance")
|
||||
else:
|
||||
# No funding transaction found, start with $0
|
||||
balance = 0.0
|
||||
logger.debug(f"[{acc.name}] CD account, no funding transaction found, using $0 starting balance")
|
||||
# --- Find the latest balance from any previous year as starting point ---
|
||||
latest_prev_balance = None
|
||||
latest_prev_date = None
|
||||
@@ -767,6 +813,7 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
||||
forecast_month = ((m - 1) % 12) + 1
|
||||
month_date = datetime(forecast_year, forecast_month, 1)
|
||||
key = (forecast_year, forecast_month)
|
||||
|
||||
# --- Universal rule: For months before the first validated balance, use $0 ---
|
||||
if earliest_actual_month is not None and forecast_year == year and forecast_month < earliest_actual_month:
|
||||
bal = 0.0
|
||||
@@ -774,13 +821,57 @@ def forecast_balances(request: schemas.ForecastRequest, db: Session = Depends(ge
|
||||
balance = bal
|
||||
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
|
||||
continue
|
||||
# --- CD logic: $0 until funded, even if historical ---
|
||||
if is_cd and (funding_month is None or forecast_month < funding_month):
|
||||
bal = 0.0
|
||||
logger.debug(f"CD {acc.name} (id={acc.id}) month {forecast_month}: Forcing balance to $0 (before funding month {funding_month})")
|
||||
balance = bal
|
||||
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
|
||||
continue
|
||||
|
||||
# --- CD logic: Check if CD should exist at this point ---
|
||||
if is_cd:
|
||||
# Check if CD has matured
|
||||
if acc.maturity_date:
|
||||
maturity_date = acc.maturity_date
|
||||
if isinstance(maturity_date, str):
|
||||
maturity_date = datetime.fromisoformat(maturity_date.replace('Z', '+00:00'))
|
||||
|
||||
# If CD has matured before this month, it should be $0
|
||||
if maturity_date and maturity_date < month_date:
|
||||
bal = 0.0
|
||||
logger.debug(f"CD {acc.name} (id={acc.id}) month {forecast_month}: Matured on {maturity_date}, balance = $0")
|
||||
balance = bal
|
||||
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
|
||||
|
||||
# Add matured CD funds to primary account if this is the maturity month
|
||||
if (maturity_date.year == forecast_year and
|
||||
maturity_date.month == forecast_month and
|
||||
primary_account_id is not None):
|
||||
# Find the CD balance just before maturity
|
||||
prev_balance = balance
|
||||
if forecast_month > 1:
|
||||
prev_month_date = datetime(forecast_year, forecast_month - 1, 1)
|
||||
prev_key = (forecast_year, forecast_month - 1)
|
||||
if prev_key in monthly_actuals:
|
||||
prev_balance = monthly_actuals[prev_key]
|
||||
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
|
||||
|
||||
# Check if CD should exist based on funding date
|
||||
if funding_month is not None:
|
||||
# Calculate the CD's age in months from funding
|
||||
cd_age_months = (forecast_year - cd_funding_tx.date.year) * 12 + (forecast_month - cd_funding_tx.date.month)
|
||||
|
||||
# If CD hasn't been funded yet, it should be $0
|
||||
if cd_age_months < 0:
|
||||
bal = 0.0
|
||||
logger.debug(f"CD {acc.name} (id={acc.id}) month {forecast_month}: Not funded yet (age: {cd_age_months} months), balance = $0")
|
||||
balance = bal
|
||||
forecast_points.append({"date": month_date.strftime("%Y-%m-%d"), "balance": bal})
|
||||
continue
|
||||
|
||||
# Use actual if available (for non-CDs, or for CDs after funding)
|
||||
if key in monthly_actuals:
|
||||
bal = monthly_actuals[key]
|
||||
|
||||
Reference in New Issue
Block a user