Files
drivers_bot/app/schemas/expense.py
2026-05-14 19:33:25 +09:00

184 lines
4.9 KiB
Python

from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel, ConfigDict, Field, model_validator
from app.models.expense import ExpenseCategory, ServiceType
class FuelEntryBase(BaseModel):
entry_date: date
odometer: int
liters: Decimal
price_per_liter: Decimal
total_cost: Decimal | None = None
station: str | None = None
fuel_brand: str | None = None
is_full_tank: bool = True
notes: str | None = None
@model_validator(mode="after")
def fill_total_cost(self) -> "FuelEntryBase":
if self.total_cost is None:
self.total_cost = self.liters * self.price_per_liter
return self
class FuelEntryCreate(FuelEntryBase):
car_id: int
class FuelEntryUpdate(BaseModel):
entry_date: date | None = None
odometer: int | None = None
liters: Decimal | None = None
price_per_liter: Decimal | None = None
total_cost: Decimal | None = None
station: str | None = None
fuel_brand: str | None = None
is_full_tank: bool | None = None
notes: str | None = None
class FuelEntryRead(FuelEntryBase):
id: int
car_id: int
total_cost: Decimal
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class ServiceEntryBase(BaseModel):
entry_date: date
odometer: int | None = None
service_type: ServiceType
title: str
category: str | None = None
vendor: str | None = None
total_cost: Decimal
next_due_date: date | None = None
next_due_odometer: int | None = None
notes: str | None = None
class ServiceEntryCreate(ServiceEntryBase):
car_id: int
class ServiceEntryUpdate(BaseModel):
entry_date: date | None = None
odometer: int | None = None
service_type: ServiceType | None = None
title: str | None = None
category: str | None = None
vendor: str | None = None
total_cost: Decimal | None = None
next_due_date: date | None = None
next_due_odometer: int | None = None
notes: str | None = None
class ServiceEntryRead(ServiceEntryBase):
id: int
car_id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class ExpenseEntryBase(BaseModel):
entry_date: date
category: ExpenseCategory
title: str
vendor: str | None = None
total_cost: Decimal
currency: str = "RUB"
odometer: int | None = None
period_start: date | None = None
period_end: date | None = None
period_months: int | None = None
is_recurring: bool = False
notes: str | None = None
@model_validator(mode="after")
def validate_period(self) -> "ExpenseEntryBase":
if self.period_months is not None and self.period_months < 1:
raise ValueError("period_months must be positive")
if self.period_start and self.period_end and self.period_end < self.period_start:
raise ValueError("period_end must be after period_start")
return self
class ExpenseEntryCreate(ExpenseEntryBase):
car_id: int
class ExpenseEntryUpdate(BaseModel):
entry_date: date | None = None
category: ExpenseCategory | None = None
title: str | None = None
vendor: str | None = None
total_cost: Decimal | None = None
currency: str | None = None
odometer: int | None = None
period_start: date | None = None
period_end: date | None = None
period_months: int | None = None
is_recurring: bool | None = None
notes: str | None = None
class ExpenseEntryRead(ExpenseEntryBase):
id: int
car_id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class OwnershipCategoryBreakdown(BaseModel):
category: str
total_cost: Decimal
entries_count: int
class OwnershipStats(BaseModel):
car_id: int
date_from: date
date_to: date
fuel_cost: Decimal
service_cost: Decimal
total_cost: Decimal
expenses_cost: Decimal = Decimal("0")
recurring_costs: Decimal = Decimal("0")
one_time_costs: Decimal = Decimal("0")
forecast_next_month: Decimal = Decimal("0")
depreciation_cost: Decimal = Decimal("0")
cost_per_month: Decimal = Decimal("0")
cost_by_category: dict[str, Decimal] = Field(default_factory=dict)
categories: list[OwnershipCategoryBreakdown] = Field(default_factory=list)
liters: Decimal
distance_km: int
avg_consumption_l_per_100km: float | None
cost_per_km: float | None
fuel_entries_count: int
service_entries_count: int
class OdometerPrediction(BaseModel):
car_id: int
samples: int
current_odometer: int | None
predicted_today: int | None
predicted_30_days: int | None
avg_km_per_day: float | None
avg_km_per_month: float | None
current_price_per_liter: float | None = None
predicted_price_per_liter_30_days: float | None = None
avg_price_per_liter: float | None = None
price_samples: int = 0
price_confidence: float = 0
confidence: float
insight: str