from datetime import date from decimal import Decimal import pandas as pd from sqlalchemy import Select, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.expense import FuelEntry, ServiceEntry from app.schemas.expense import OdometerPrediction, OwnershipStats async def get_ownership_stats( session: AsyncSession, car_id: int, date_from: date, date_to: date ) -> OwnershipStats: fuel_totals = await session.execute( select( func.coalesce(func.sum(FuelEntry.total_cost), 0), func.coalesce(func.sum(FuelEntry.liters), 0), func.count(FuelEntry.id), func.min(FuelEntry.odometer), func.max(FuelEntry.odometer), ).where( FuelEntry.car_id == car_id, FuelEntry.entry_date >= date_from, FuelEntry.entry_date <= date_to, ) ) fuel_cost, liters, fuel_count, min_odo, max_odo = fuel_totals.one() service_totals = await session.execute( select(func.coalesce(func.sum(ServiceEntry.total_cost), 0), func.count(ServiceEntry.id)).where( ServiceEntry.car_id == car_id, ServiceEntry.entry_date >= date_from, ServiceEntry.entry_date <= date_to, ) ) service_cost, service_count = service_totals.one() distance_km = int(max_odo - min_odo) if min_odo is not None and max_odo is not None else 0 total_cost = Decimal(fuel_cost) + Decimal(service_cost) avg_consumption = float(Decimal(liters) * Decimal(100) / distance_km) if distance_km else None cost_per_km = float(total_cost / distance_km) if distance_km else None return OwnershipStats( car_id=car_id, date_from=date_from, date_to=date_to, fuel_cost=fuel_cost, service_cost=service_cost, total_cost=total_cost, liters=liters, distance_km=distance_km, avg_consumption_l_per_100km=avg_consumption, cost_per_km=cost_per_km, fuel_entries_count=fuel_count, service_entries_count=service_count, ) async def dataframe_from_query(session: AsyncSession, stmt: Select) -> pd.DataFrame: result = await session.execute(stmt) rows = result.mappings().all() return pd.DataFrame(rows) async def predict_odometer(session: AsyncSession, car_id: int) -> OdometerPrediction: fuel = await dataframe_from_query( session, select(FuelEntry.entry_date.label("date"), FuelEntry.odometer.label("odometer")).where( FuelEntry.car_id == car_id ), ) service = await dataframe_from_query( session, select(ServiceEntry.entry_date.label("date"), ServiceEntry.odometer.label("odometer")).where( ServiceEntry.car_id == car_id, ServiceEntry.odometer.is_not(None) ), ) if fuel.empty and service.empty: return OdometerPrediction( car_id=car_id, samples=0, current_odometer=None, predicted_today=None, predicted_30_days=None, avg_km_per_day=None, avg_km_per_month=None, confidence=0, insight="Недостаточно данных: добавь одометр в заправках или сервисных записях.", ) df = pd.concat([fuel, service]).dropna().drop_duplicates().sort_values("date") df["date"] = pd.to_datetime(df["date"]) df = df.sort_values(["date", "odometer"]).drop_duplicates(subset=["date"], keep="last") if len(df) < 2: current = int(df.iloc[-1]["odometer"]) return OdometerPrediction( car_id=car_id, samples=len(df), current_odometer=current, predicted_today=current, predicted_30_days=None, avg_km_per_day=None, avg_km_per_month=None, confidence=0.2, insight="Есть только одна точка пробега. Для прогноза нужны минимум две записи.", ) first = df.iloc[0] last = df.iloc[-1] days = max((last["date"] - first["date"]).days, 1) distance = max(int(last["odometer"] - first["odometer"]), 0) km_per_day = distance / days today = pd.Timestamp.utcnow().tz_localize(None).normalize() days_since_last = max((today - last["date"]).days, 0) predicted_today = int(last["odometer"] + km_per_day * days_since_last) predicted_30 = int(predicted_today + km_per_day * 30) confidence = min(0.95, 0.35 + len(df) * 0.035 + min(days, 365) / 730) insight = ( "Пробег стабилен, прогноз надежный." if confidence >= 0.75 else "Прогноз предварительный: точность вырастет после нескольких новых записей." ) return OdometerPrediction( car_id=car_id, samples=len(df), current_odometer=int(last["odometer"]), predicted_today=predicted_today, predicted_30_days=predicted_30, avg_km_per_day=round(km_per_day, 1), avg_km_per_month=round(km_per_day * 30.4, 1), confidence=round(confidence, 2), insight=insight, )