158 lines
5.0 KiB
Python
158 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.car import Car, OdometerHistory
|
|
from app.models.expense import ExpenseEntry, FuelEntry, ServiceEntry
|
|
|
|
|
|
def validate_odometer_change(
|
|
car: Car,
|
|
new_odometer: int | None,
|
|
*,
|
|
source_record_type: str,
|
|
confirm_lower_odometer: bool = False,
|
|
) -> None:
|
|
if new_odometer is None:
|
|
return
|
|
if new_odometer < 0:
|
|
raise HTTPException(status_code=422, detail="Odometer must be non-negative")
|
|
current = car.current_odometer
|
|
if current is not None and new_odometer < current and not confirm_lower_odometer:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail={
|
|
"code": "odometer_lower_than_current",
|
|
"message": "Новый пробег меньше текущего. Подтвердите ручную корректировку или проверьте запись.",
|
|
"current_odometer": current,
|
|
"new_odometer": new_odometer,
|
|
"source": source_record_type,
|
|
},
|
|
)
|
|
if current is not None and new_odometer > current + 100000 and not confirm_lower_odometer:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail={
|
|
"code": "odometer_jump_requires_confirmation",
|
|
"message": "Пробег сильно отличается от текущего. Проверьте число перед сохранением.",
|
|
"current_odometer": current,
|
|
"new_odometer": new_odometer,
|
|
"source": source_record_type,
|
|
},
|
|
)
|
|
|
|
|
|
def add_odometer_history(
|
|
session: AsyncSession,
|
|
car: Car,
|
|
*,
|
|
new_odometer: int,
|
|
source_record_type: str,
|
|
source_record_id: int | None,
|
|
changed_by: int | None,
|
|
confirmation_required: bool = False,
|
|
user_confirmed: bool = True,
|
|
) -> None:
|
|
previous = car.current_odometer
|
|
session.add(
|
|
OdometerHistory(
|
|
car_id=car.id,
|
|
previous_odometer=previous,
|
|
new_odometer=new_odometer,
|
|
source_record_type=source_record_type,
|
|
source_record_id=source_record_id,
|
|
changed_by=changed_by,
|
|
confirmation_required=confirmation_required,
|
|
user_confirmed=user_confirmed,
|
|
)
|
|
)
|
|
car.current_odometer = new_odometer
|
|
|
|
|
|
async def apply_odometer_from_record(
|
|
session: AsyncSession,
|
|
car: Car,
|
|
*,
|
|
new_odometer: int | None,
|
|
source_record_type: str,
|
|
source_record_id: int | None,
|
|
changed_by: int | None,
|
|
confirm_lower_odometer: bool = False,
|
|
) -> None:
|
|
if new_odometer is None:
|
|
return
|
|
validate_odometer_change(
|
|
car,
|
|
new_odometer,
|
|
source_record_type=source_record_type,
|
|
confirm_lower_odometer=confirm_lower_odometer,
|
|
)
|
|
current = car.current_odometer
|
|
if current is None or new_odometer > current or confirm_lower_odometer:
|
|
add_odometer_history(
|
|
session,
|
|
car,
|
|
new_odometer=new_odometer,
|
|
source_record_type=source_record_type,
|
|
source_record_id=source_record_id,
|
|
changed_by=changed_by,
|
|
confirmation_required=current is not None and new_odometer < current,
|
|
user_confirmed=True,
|
|
)
|
|
|
|
|
|
async def recalculate_current_odometer(
|
|
session: AsyncSession,
|
|
car_id: int,
|
|
*,
|
|
changed_by: int | None = None,
|
|
source_record_type: str = "recalculate",
|
|
) -> None:
|
|
car = await session.get(Car, car_id)
|
|
if car is None:
|
|
return
|
|
fuel_result = await session.execute(
|
|
select(FuelEntry.odometer)
|
|
.where(FuelEntry.car_id == car_id)
|
|
.order_by(FuelEntry.odometer.desc())
|
|
.limit(1)
|
|
)
|
|
service_result = await session.execute(
|
|
select(ServiceEntry.odometer)
|
|
.where(ServiceEntry.car_id == car_id, ServiceEntry.odometer.is_not(None))
|
|
.order_by(ServiceEntry.odometer.desc())
|
|
.limit(1)
|
|
)
|
|
expense_result = await session.execute(
|
|
select(ExpenseEntry.odometer)
|
|
.where(ExpenseEntry.car_id == car_id, ExpenseEntry.odometer.is_not(None))
|
|
.order_by(ExpenseEntry.odometer.desc())
|
|
.limit(1)
|
|
)
|
|
values = [
|
|
value
|
|
for value in (
|
|
fuel_result.scalar_one_or_none(),
|
|
service_result.scalar_one_or_none(),
|
|
expense_result.scalar_one_or_none(),
|
|
)
|
|
if value is not None
|
|
]
|
|
new_value = max(values) if values else None
|
|
if new_value != car.current_odometer:
|
|
if new_value is None:
|
|
car.current_odometer = None
|
|
return
|
|
add_odometer_history(
|
|
session,
|
|
car,
|
|
new_odometer=new_value,
|
|
source_record_type=source_record_type,
|
|
source_record_id=None,
|
|
changed_by=changed_by,
|
|
confirmation_required=False,
|
|
user_confirmed=True,
|
|
)
|