268 lines
8.3 KiB
Python
268 lines
8.3 KiB
Python
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, 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 | None = None
|
|
notes: str | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def fill_total_cost(self) -> "FuelEntryBase":
|
|
if self.odometer < 0:
|
|
raise ValueError("odometer must be non-negative")
|
|
if self.liters <= 0:
|
|
raise ValueError("liters must be positive")
|
|
if self.price_per_liter <= 0:
|
|
raise ValueError("price_per_liter must be positive")
|
|
if self.total_cost is None:
|
|
self.total_cost = self.liters * self.price_per_liter
|
|
if self.total_cost <= 0:
|
|
raise ValueError("total_cost must be positive")
|
|
return self
|
|
|
|
|
|
class FuelEntryCreate(FuelEntryBase):
|
|
car_id: int
|
|
confirm_lower_odometer: bool = False
|
|
|
|
|
|
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
|
|
confirm_lower_odometer: bool = False
|
|
|
|
|
|
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
|
|
|
|
@model_validator(mode="after")
|
|
def validate_service(self) -> "ServiceEntryBase":
|
|
if self.odometer is not None and self.odometer < 0:
|
|
raise ValueError("odometer must be non-negative")
|
|
if self.total_cost < 0:
|
|
raise ValueError("total_cost must be non-negative")
|
|
if not self.title.strip():
|
|
raise ValueError("title is required")
|
|
return self
|
|
|
|
|
|
class ServiceEntryCreate(ServiceEntryBase):
|
|
car_id: int
|
|
confirm_lower_odometer: bool = False
|
|
|
|
|
|
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
|
|
confirm_lower_odometer: bool = False
|
|
|
|
|
|
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
|
|
policy_number: str | None = None
|
|
insurance_type: str | None = None
|
|
payment_period_months: int | None = None
|
|
document_urls: list[str] | None = None
|
|
metadata_json: dict | None = None
|
|
notes: str | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def validate_period(self) -> "ExpenseEntryBase":
|
|
if self.total_cost <= 0:
|
|
raise ValueError("total_cost must be positive")
|
|
if self.odometer is not None and self.odometer < 0:
|
|
raise ValueError("odometer must be non-negative")
|
|
if self.period_months is not None and self.period_months < 1:
|
|
raise ValueError("period_months must be positive")
|
|
if self.payment_period_months is not None and self.payment_period_months < 1:
|
|
raise ValueError("payment_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")
|
|
if self.category == ExpenseCategory.insurance:
|
|
if self.period_start and self.period_end:
|
|
return self
|
|
if self.period_months or self.payment_period_months:
|
|
return self
|
|
return self
|
|
|
|
|
|
class ExpenseEntryCreate(ExpenseEntryBase):
|
|
car_id: int
|
|
confirm_lower_odometer: bool = False
|
|
|
|
|
|
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
|
|
policy_number: str | None = None
|
|
insurance_type: str | None = None
|
|
payment_period_months: int | None = None
|
|
document_urls: list[str] | None = None
|
|
metadata_json: dict | None = None
|
|
notes: str | None = None
|
|
confirm_lower_odometer: bool = False
|
|
|
|
@field_validator("total_cost")
|
|
@classmethod
|
|
def validate_total_cost(cls, value: Decimal | None) -> Decimal | None:
|
|
if value is not None and value <= 0:
|
|
raise ValueError("total_cost must be positive")
|
|
return value
|
|
|
|
|
|
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")
|
|
repair_cost: Decimal = Decimal("0")
|
|
fixed_costs: Decimal = Decimal("0")
|
|
variable_costs: 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")
|
|
loan_principal_cost: Decimal = Decimal("0")
|
|
loan_interest_cost: Decimal = Decimal("0")
|
|
total_cost_without_credit: Decimal = Decimal("0")
|
|
total_cost_with_credit: Decimal = Decimal("0")
|
|
cost_per_day: Decimal = Decimal("0")
|
|
cost_per_month: Decimal = Decimal("0")
|
|
current_month_cost: Decimal = Decimal("0")
|
|
previous_month_cost: Decimal = Decimal("0")
|
|
month_over_month_change_pct: float | None = None
|
|
cost_warning: str | None = None
|
|
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
|
|
average_full_tank_distance: float | None = None
|
|
average_fuel_consumption_full_tank: float | None = None
|
|
average_cost_per_full_tank: float | None = None
|
|
last_full_tank_distance: int | None = None
|
|
full_tank_warning: str | None = None
|
|
confidence: float
|
|
insight: str
|
|
|
|
|
|
class OdometerHistoryRead(BaseModel):
|
|
id: int
|
|
car_id: int
|
|
previous_odometer: int | None = None
|
|
new_odometer: int
|
|
source_record_type: str
|
|
source_record_id: int | None = None
|
|
changed_at: datetime
|
|
changed_by: int | None = None
|
|
confirmation_required: bool
|
|
user_confirmed: bool
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|