Forecasting app: hybrid port to HNF stack
Python FastAPI ML backend kept intact; auth replaced with central hnf_session cookie verification. Frontend rebuilt on React 18 + TS + Vite with stack design system, Plotly charts retained. Shared Postgres via DATABASE_URL; schema applied on startup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
75d2c1fa9d
103 changed files with 70316 additions and 0 deletions
537
backend/api/public.py
Normal file
537
backend/api/public.py
Normal file
|
|
@ -0,0 +1,537 @@
|
|||
"""
|
||||
Public API endpoints - accessible with API key authentication
|
||||
For external integrations like Kitchen Flash app
|
||||
"""
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
from database import get_db
|
||||
from auth import get_api_key_auth
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_prior_year_date(target_date: date) -> date:
|
||||
"""
|
||||
Get prior year date with 364-day offset for day-of-week alignment.
|
||||
52 weeks = 364 days, so Monday aligns with Monday.
|
||||
"""
|
||||
return target_date - timedelta(days=364)
|
||||
|
||||
|
||||
@router.get("/forecast/rooms")
|
||||
async def get_rooms_forecast(
|
||||
start_date: str = Query(..., description="Start date (YYYY-MM-DD)"),
|
||||
days: int = Query(7, ge=1, le=365, description="Number of days to forecast"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: dict = Depends(get_api_key_auth)
|
||||
):
|
||||
"""
|
||||
Get forecasted room bookings for external applications.
|
||||
|
||||
Returns: OTB rooms, forecast rooms, occupancy %, prior year data
|
||||
"""
|
||||
from services.forecasting.pickup_v2_model import forecast_rooms_for_date
|
||||
|
||||
try:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
|
||||
|
||||
end = start + timedelta(days=days - 1)
|
||||
today = date.today()
|
||||
|
||||
# Get total rooms for occupancy calculation
|
||||
result = await db.execute(
|
||||
text("SELECT config_value FROM system_config WHERE config_key = 'total_rooms'")
|
||||
)
|
||||
row = result.fetchone()
|
||||
total_rooms = int(row.config_value) if row and row.config_value else 30
|
||||
|
||||
data = []
|
||||
current = start
|
||||
while current <= end:
|
||||
lead_days = (current - today).days if current >= today else 0
|
||||
prior_date = get_prior_year_date(current)
|
||||
|
||||
try:
|
||||
if current >= today:
|
||||
# Future date - get forecast
|
||||
forecast = await forecast_rooms_for_date(
|
||||
db, current, lead_days, prior_date, 'hotel_room_nights'
|
||||
)
|
||||
otb_rooms = forecast.get('current_otb', 0) or 0
|
||||
pickup_rooms = forecast.get('expected_pickup', 0) or 0
|
||||
forecast_rooms = otb_rooms + pickup_rooms
|
||||
prior_otb = forecast.get('prior_year_otb', 0) or 0
|
||||
prior_final = forecast.get('prior_year_final', 0) or 0
|
||||
|
||||
# Get guest counts from stats
|
||||
stats_result = await db.execute(
|
||||
text("""
|
||||
SELECT booking_count, guests_count FROM newbook_bookings_stats
|
||||
WHERE date = :target_date
|
||||
"""),
|
||||
{"target_date": current}
|
||||
)
|
||||
stats_row = stats_result.fetchone()
|
||||
otb_guests = stats_row.guests_count if stats_row and stats_row.guests_count else 0
|
||||
|
||||
# Prior year guests for ratio
|
||||
prior_stats = await db.execute(
|
||||
text("""
|
||||
SELECT booking_count, guests_count FROM newbook_bookings_stats
|
||||
WHERE date = :prior_date
|
||||
"""),
|
||||
{"prior_date": prior_date}
|
||||
)
|
||||
prior_stats_row = prior_stats.fetchone()
|
||||
prior_guests = prior_stats_row.guests_count if prior_stats_row and prior_stats_row.guests_count else 0
|
||||
prior_rooms_actual = prior_stats_row.booking_count if prior_stats_row and prior_stats_row.booking_count else 0
|
||||
|
||||
# Calculate guests per room ratio (prior year -> current OTB -> default)
|
||||
if prior_rooms_actual > 0:
|
||||
guests_per_room = prior_guests / prior_rooms_actual
|
||||
elif otb_rooms > 0:
|
||||
guests_per_room = otb_guests / otb_rooms
|
||||
else:
|
||||
guests_per_room = 1.8
|
||||
|
||||
pickup_guests = round(pickup_rooms * guests_per_room)
|
||||
forecast_guests = otb_guests + pickup_guests
|
||||
else:
|
||||
# Past date - get actuals from stats
|
||||
stats_result = await db.execute(
|
||||
text("""
|
||||
SELECT booking_count, guests_count FROM newbook_bookings_stats
|
||||
WHERE date = :target_date
|
||||
"""),
|
||||
{"target_date": current}
|
||||
)
|
||||
stats_row = stats_result.fetchone()
|
||||
otb_rooms = stats_row.booking_count if stats_row else 0
|
||||
otb_guests = stats_row.guests_count if stats_row and stats_row.guests_count else 0
|
||||
forecast_rooms = otb_rooms
|
||||
forecast_guests = otb_guests
|
||||
pickup_rooms = 0
|
||||
pickup_guests = 0
|
||||
|
||||
# Prior year stats
|
||||
prior_result = await db.execute(
|
||||
text("""
|
||||
SELECT booking_count FROM newbook_bookings_stats
|
||||
WHERE date = :prior_date
|
||||
"""),
|
||||
{"prior_date": prior_date}
|
||||
)
|
||||
prior_row = prior_result.fetchone()
|
||||
prior_final = prior_row.booking_count if prior_row else 0
|
||||
prior_otb = prior_final
|
||||
|
||||
occupancy_pct = round((forecast_rooms / total_rooms) * 100, 1) if total_rooms > 0 else 0
|
||||
|
||||
data.append({
|
||||
"date": current.isoformat(),
|
||||
"day": current.strftime("%A"),
|
||||
"lead_days": lead_days,
|
||||
"otb_rooms": otb_rooms,
|
||||
"pickup_rooms": pickup_rooms,
|
||||
"forecast_rooms": forecast_rooms,
|
||||
"otb_guests": otb_guests,
|
||||
"pickup_guests": pickup_guests,
|
||||
"forecast_guests": forecast_guests,
|
||||
"occupancy_pct": occupancy_pct,
|
||||
"prior_year_otb": prior_otb,
|
||||
"prior_year_final": prior_final,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error forecasting rooms for {current}: {e}")
|
||||
data.append({
|
||||
"date": current.isoformat(),
|
||||
"day": current.strftime("%A"),
|
||||
"lead_days": lead_days,
|
||||
"otb_rooms": 0,
|
||||
"pickup_rooms": 0,
|
||||
"forecast_rooms": 0,
|
||||
"otb_guests": 0,
|
||||
"pickup_guests": 0,
|
||||
"forecast_guests": 0,
|
||||
"occupancy_pct": 0,
|
||||
"prior_year_otb": 0,
|
||||
"prior_year_final": 0,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
current += timedelta(days=1)
|
||||
|
||||
return {"data": data, "total_rooms": total_rooms}
|
||||
|
||||
|
||||
@router.get("/forecast/covers")
|
||||
async def get_covers_forecast(
|
||||
start_date: str = Query(..., description="Start date (YYYY-MM-DD)"),
|
||||
days: int = Query(7, ge=1, le=365, description="Number of days to forecast"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: dict = Depends(get_api_key_auth)
|
||||
):
|
||||
"""
|
||||
Get forecasted restaurant covers by period for external applications.
|
||||
|
||||
Returns: OTB covers, forecast covers, prior year data for breakfast, lunch, dinner
|
||||
"""
|
||||
from services.forecasting.covers_model import forecast_covers_range
|
||||
|
||||
try:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
|
||||
|
||||
end = start + timedelta(days=days - 1)
|
||||
today = date.today()
|
||||
|
||||
try:
|
||||
# Get covers forecast
|
||||
covers_data = await forecast_covers_range(db, start, end, include_details=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Covers forecast failed: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Forecast error: {str(e)}")
|
||||
|
||||
# Get prior year covers from stats
|
||||
prior_start = get_prior_year_date(start)
|
||||
prior_end = get_prior_year_date(end)
|
||||
|
||||
prior_result = await db.execute(
|
||||
text("""
|
||||
SELECT date, breakfast_covers, lunch_covers, dinner_covers
|
||||
FROM resos_bookings_stats
|
||||
WHERE date >= :start_date AND date <= :end_date
|
||||
"""),
|
||||
{"start_date": prior_start, "end_date": prior_end}
|
||||
)
|
||||
prior_rows = prior_result.fetchall()
|
||||
prior_by_date = {row.date: row for row in prior_rows}
|
||||
|
||||
data = []
|
||||
for day_data in covers_data.get("data", []):
|
||||
target_date = datetime.strptime(day_data["date"], "%Y-%m-%d").date()
|
||||
prior_date = get_prior_year_date(target_date)
|
||||
lead_days = (target_date - today).days if target_date >= today else 0
|
||||
|
||||
prior_row = prior_by_date.get(prior_date)
|
||||
prior_breakfast = prior_row.breakfast_covers if prior_row else 0
|
||||
prior_lunch = prior_row.lunch_covers if prior_row else 0
|
||||
prior_dinner = prior_row.dinner_covers if prior_row else 0
|
||||
|
||||
# For prior OTB, we don't have pace data, so use same as final for simplicity
|
||||
data.append({
|
||||
"date": day_data["date"],
|
||||
"day": day_data["day_of_week"],
|
||||
"lead_days": lead_days,
|
||||
"breakfast": {
|
||||
"otb": day_data["breakfast"]["otb"],
|
||||
"forecast": day_data["breakfast"]["forecast"],
|
||||
"prior_otb": prior_breakfast, # Same as final for prior year
|
||||
"prior_final": prior_breakfast
|
||||
},
|
||||
"lunch": {
|
||||
"otb": day_data["lunch"]["otb"],
|
||||
"forecast": day_data["lunch"]["forecast"],
|
||||
"prior_otb": prior_lunch,
|
||||
"prior_final": prior_lunch
|
||||
},
|
||||
"dinner": {
|
||||
"otb": day_data["dinner"]["otb"],
|
||||
"forecast": day_data["dinner"]["forecast"],
|
||||
"prior_otb": prior_dinner,
|
||||
"prior_final": prior_dinner
|
||||
},
|
||||
"total": {
|
||||
"otb": day_data["totals"]["otb"],
|
||||
"forecast": day_data["totals"]["forecast"],
|
||||
"prior_final": prior_breakfast + prior_lunch + prior_dinner
|
||||
}
|
||||
})
|
||||
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@router.get("/forecast/revenue")
|
||||
async def get_revenue_forecast(
|
||||
start_date: str = Query(..., description="Start date (YYYY-MM-DD)"),
|
||||
days: int = Query(7, ge=1, le=365, description="Number of days to forecast"),
|
||||
type: str = Query("all", description="Revenue type: all, accom, dry, wet"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: dict = Depends(get_api_key_auth)
|
||||
):
|
||||
"""
|
||||
Get forecasted revenue by type for external applications.
|
||||
|
||||
Returns: OTB revenue, forecast revenue, prior year actuals, budget
|
||||
"""
|
||||
from services.forecasting.covers_model import forecast_covers_range
|
||||
from services.forecasting.pickup_v2_model import forecast_revenue_for_date
|
||||
|
||||
try:
|
||||
start = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
|
||||
|
||||
end = start + timedelta(days=days - 1)
|
||||
today = date.today()
|
||||
VAT_RATE = 1.20
|
||||
|
||||
# Get spend settings for restaurant revenue
|
||||
spend_result = await db.execute(
|
||||
text("""
|
||||
SELECT config_key, config_value
|
||||
FROM system_config
|
||||
WHERE config_key LIKE 'resos_%_spend'
|
||||
""")
|
||||
)
|
||||
spend_rows = spend_result.fetchall()
|
||||
spend_settings = {row.config_key: float(row.config_value or 0) for row in spend_rows}
|
||||
|
||||
def get_spend_by_period(period: str, revenue_type: str) -> float:
|
||||
if revenue_type == 'dry':
|
||||
return spend_settings.get(f'resos_{period}_food_spend', 0) / VAT_RATE
|
||||
elif revenue_type == 'wet':
|
||||
return spend_settings.get(f'resos_{period}_drinks_spend', 0) / VAT_RATE
|
||||
else:
|
||||
food = spend_settings.get(f'resos_{period}_food_spend', 0)
|
||||
drinks = spend_settings.get(f'resos_{period}_drinks_spend', 0)
|
||||
return (food + drinks) / VAT_RATE
|
||||
|
||||
# Get actual revenue
|
||||
actual_result = await db.execute(
|
||||
text("""
|
||||
SELECT date, accommodation, dry, wet
|
||||
FROM newbook_net_revenue_data
|
||||
WHERE date >= :start_date AND date <= :end_date
|
||||
"""),
|
||||
{"start_date": start, "end_date": end}
|
||||
)
|
||||
actual_by_date = {row.date: row for row in actual_result.fetchall()}
|
||||
|
||||
# Get prior year revenue
|
||||
prior_start = get_prior_year_date(start)
|
||||
prior_end = get_prior_year_date(end)
|
||||
prior_result = await db.execute(
|
||||
text("""
|
||||
SELECT date, accommodation, dry, wet
|
||||
FROM newbook_net_revenue_data
|
||||
WHERE date >= :start_date AND date <= :end_date
|
||||
"""),
|
||||
{"start_date": prior_start, "end_date": prior_end}
|
||||
)
|
||||
prior_by_date = {row.date: row for row in prior_result.fetchall()}
|
||||
|
||||
# Get budgets
|
||||
budget_types = []
|
||||
if type in ['all', 'accom']:
|
||||
budget_types.append('net_accom')
|
||||
if type in ['all', 'dry']:
|
||||
budget_types.append('net_dry')
|
||||
if type in ['all', 'wet']:
|
||||
budget_types.append('net_wet')
|
||||
if type == 'all':
|
||||
budget_types.append('total_rev')
|
||||
|
||||
budget_result = await db.execute(
|
||||
text("""
|
||||
SELECT date, budget_type, budget_value
|
||||
FROM daily_budgets
|
||||
WHERE date >= :start_date AND date <= :end_date
|
||||
AND budget_type = ANY(:types)
|
||||
"""),
|
||||
{"start_date": start, "end_date": end, "types": budget_types if budget_types else ['']}
|
||||
)
|
||||
budget_data = {}
|
||||
for row in budget_result.fetchall():
|
||||
if row.date not in budget_data:
|
||||
budget_data[row.date] = {}
|
||||
budget_data[row.date][row.budget_type] = float(row.budget_value or 0)
|
||||
|
||||
# Get covers forecast for restaurant revenue
|
||||
try:
|
||||
covers_data = await forecast_covers_range(db, start, end, include_details=False)
|
||||
covers_by_date = {c["date"]: c for c in covers_data.get("data", [])}
|
||||
except Exception as e:
|
||||
logger.warning(f"Covers forecast failed: {e}")
|
||||
covers_by_date = {}
|
||||
|
||||
data = []
|
||||
current = start
|
||||
while current <= end:
|
||||
is_past = current < today
|
||||
lead_days = (current - today).days if current >= today else 0
|
||||
prior_date = get_prior_year_date(current)
|
||||
|
||||
# Prior year actual
|
||||
prior_row = prior_by_date.get(prior_date)
|
||||
prior_accom = float(prior_row.accommodation) if prior_row and prior_row.accommodation else 0
|
||||
prior_dry = float(prior_row.dry) if prior_row and prior_row.dry else 0
|
||||
prior_wet = float(prior_row.wet) if prior_row and prior_row.wet else 0
|
||||
|
||||
# Budget
|
||||
day_budget = budget_data.get(current, {})
|
||||
budget_accom = day_budget.get('net_accom', 0)
|
||||
budget_dry = day_budget.get('net_dry', 0)
|
||||
budget_wet = day_budget.get('net_wet', 0)
|
||||
budget_total = day_budget.get('total_rev', budget_accom + budget_dry + budget_wet)
|
||||
|
||||
if is_past:
|
||||
# Past: use actual revenue
|
||||
actual_row = actual_by_date.get(current)
|
||||
accom_otb = float(actual_row.accommodation) if actual_row and actual_row.accommodation else 0
|
||||
dry_otb = float(actual_row.dry) if actual_row and actual_row.dry else 0
|
||||
wet_otb = float(actual_row.wet) if actual_row and actual_row.wet else 0
|
||||
accom_forecast = accom_otb
|
||||
dry_forecast = dry_otb
|
||||
wet_forecast = wet_otb
|
||||
accom_prior_otb = prior_accom
|
||||
else:
|
||||
# Future: forecast
|
||||
# Accommodation - use revenue forecast model
|
||||
try:
|
||||
accom_forecast_data = await forecast_revenue_for_date(
|
||||
db, current, lead_days, prior_date
|
||||
)
|
||||
accom_otb = accom_forecast_data.get('current_otb_rev', 0) or 0
|
||||
accom_pickup = accom_forecast_data.get('forecast_pickup_rev', 0) or 0
|
||||
accom_forecast = accom_otb + accom_pickup
|
||||
accom_prior_otb = accom_forecast_data.get('prior_year_otb_rev', 0) or 0
|
||||
except Exception:
|
||||
accom_otb = accom_forecast = accom_prior_otb = 0
|
||||
|
||||
# Restaurant
|
||||
day_covers = covers_by_date.get(current.isoformat())
|
||||
if day_covers:
|
||||
breakfast_otb = day_covers["breakfast"]["otb"]
|
||||
lunch_otb = day_covers["lunch"]["otb"]
|
||||
dinner_otb = day_covers["dinner"]["otb"]
|
||||
breakfast_forecast = day_covers["breakfast"]["forecast"]
|
||||
lunch_forecast = day_covers["lunch"]["forecast"]
|
||||
dinner_forecast = day_covers["dinner"]["forecast"]
|
||||
|
||||
dry_otb = (
|
||||
breakfast_otb * get_spend_by_period('breakfast', 'dry') +
|
||||
lunch_otb * get_spend_by_period('lunch', 'dry') +
|
||||
dinner_otb * get_spend_by_period('dinner', 'dry')
|
||||
)
|
||||
dry_forecast = (
|
||||
breakfast_forecast * get_spend_by_period('breakfast', 'dry') +
|
||||
lunch_forecast * get_spend_by_period('lunch', 'dry') +
|
||||
dinner_forecast * get_spend_by_period('dinner', 'dry')
|
||||
)
|
||||
wet_otb = (
|
||||
breakfast_otb * get_spend_by_period('breakfast', 'wet') +
|
||||
lunch_otb * get_spend_by_period('lunch', 'wet') +
|
||||
dinner_otb * get_spend_by_period('dinner', 'wet')
|
||||
)
|
||||
wet_forecast = (
|
||||
breakfast_forecast * get_spend_by_period('breakfast', 'wet') +
|
||||
lunch_forecast * get_spend_by_period('lunch', 'wet') +
|
||||
dinner_forecast * get_spend_by_period('dinner', 'wet')
|
||||
)
|
||||
else:
|
||||
dry_otb = dry_forecast = wet_otb = wet_forecast = 0
|
||||
|
||||
day_data = {
|
||||
"date": current.isoformat(),
|
||||
"day": current.strftime("%A"),
|
||||
"is_past": is_past,
|
||||
"lead_days": lead_days,
|
||||
}
|
||||
|
||||
if type in ['all', 'accom']:
|
||||
day_data["accom"] = {
|
||||
"otb": round(accom_otb, 2),
|
||||
"forecast": round(accom_forecast, 2),
|
||||
"prior_otb": round(accom_prior_otb, 2) if not is_past else round(prior_accom, 2),
|
||||
"prior_final": round(prior_accom, 2),
|
||||
"budget": round(budget_accom, 2)
|
||||
}
|
||||
|
||||
if type in ['all', 'dry']:
|
||||
day_data["dry"] = {
|
||||
"otb": round(dry_otb, 2),
|
||||
"forecast": round(dry_forecast, 2),
|
||||
"prior_final": round(prior_dry, 2),
|
||||
"budget": round(budget_dry, 2)
|
||||
}
|
||||
|
||||
if type in ['all', 'wet']:
|
||||
day_data["wet"] = {
|
||||
"otb": round(wet_otb, 2),
|
||||
"forecast": round(wet_forecast, 2),
|
||||
"prior_final": round(prior_wet, 2),
|
||||
"budget": round(budget_wet, 2)
|
||||
}
|
||||
|
||||
if type == 'all':
|
||||
total_otb = accom_otb + dry_otb + wet_otb
|
||||
total_forecast = accom_forecast + dry_forecast + wet_forecast
|
||||
total_prior = prior_accom + prior_dry + prior_wet
|
||||
day_data["total"] = {
|
||||
"otb": round(total_otb, 2),
|
||||
"forecast": round(total_forecast, 2),
|
||||
"prior_final": round(total_prior, 2),
|
||||
"budget": round(budget_total, 2)
|
||||
}
|
||||
|
||||
data.append(day_data)
|
||||
current += timedelta(days=1)
|
||||
|
||||
return {"data": data}
|
||||
|
||||
|
||||
@router.get("/forecast/spend-rates")
|
||||
async def get_spend_rates(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: dict = Depends(get_api_key_auth)
|
||||
):
|
||||
"""
|
||||
Return spend-per-cover rates for each meal period.
|
||||
Values are gross (inc VAT) from system_config, plus the VAT rate.
|
||||
Kitchen app can divide by VAT rate to get net values.
|
||||
"""
|
||||
VAT_RATE = 1.20
|
||||
|
||||
spend_result = await db.execute(
|
||||
text("""
|
||||
SELECT config_key, config_value
|
||||
FROM system_config
|
||||
WHERE config_key LIKE 'resos_%_spend'
|
||||
""")
|
||||
)
|
||||
spend_rows = spend_result.fetchall()
|
||||
spend_settings = {row.config_key: float(row.config_value or 0) for row in spend_rows}
|
||||
|
||||
return {
|
||||
"vat_rate": VAT_RATE,
|
||||
"periods": {
|
||||
"breakfast": {
|
||||
"food_spend_gross": spend_settings.get("resos_breakfast_food_spend", 0),
|
||||
"drinks_spend_gross": spend_settings.get("resos_breakfast_drinks_spend", 0),
|
||||
"food_spend_net": round(spend_settings.get("resos_breakfast_food_spend", 0) / VAT_RATE, 2),
|
||||
"drinks_spend_net": round(spend_settings.get("resos_breakfast_drinks_spend", 0) / VAT_RATE, 2),
|
||||
},
|
||||
"lunch": {
|
||||
"food_spend_gross": spend_settings.get("resos_lunch_food_spend", 0),
|
||||
"drinks_spend_gross": spend_settings.get("resos_lunch_drinks_spend", 0),
|
||||
"food_spend_net": round(spend_settings.get("resos_lunch_food_spend", 0) / VAT_RATE, 2),
|
||||
"drinks_spend_net": round(spend_settings.get("resos_lunch_drinks_spend", 0) / VAT_RATE, 2),
|
||||
},
|
||||
"dinner": {
|
||||
"food_spend_gross": spend_settings.get("resos_dinner_food_spend", 0),
|
||||
"drinks_spend_gross": spend_settings.get("resos_dinner_drinks_spend", 0),
|
||||
"food_spend_net": round(spend_settings.get("resos_dinner_food_spend", 0) / VAT_RATE, 2),
|
||||
"drinks_spend_net": round(spend_settings.get("resos_dinner_drinks_spend", 0) / VAT_RATE, 2),
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue