591 lines
25 KiB
Python
591 lines
25 KiB
Python
from datetime import UTC, datetime
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||
from sqlalchemy import or_, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy.orm import selectinload
|
||
|
||
from app.api.deps import ensure_service_employee, get_current_telegram_user, log_audit
|
||
from app.core.config import settings
|
||
from app.db.session import get_session
|
||
from app.models.car import (
|
||
Car,
|
||
CarServiceLink,
|
||
ServiceAppointment,
|
||
ServiceCenter,
|
||
ServiceProductItem,
|
||
ServiceVisit,
|
||
ServiceWorkItem,
|
||
WorkOrderCatalogItem,
|
||
WorkOrderCorrection,
|
||
WorkOrderStatusHistory,
|
||
)
|
||
from app.models.user import User
|
||
from app.schemas.service_center import (
|
||
ServiceProductItemCreate,
|
||
ServiceProductItemRead,
|
||
ServiceVisitRead,
|
||
ServiceWorkItemCreate,
|
||
ServiceWorkItemRead,
|
||
VehicleProfileRequest,
|
||
WorkOrderCatalogItemCreate,
|
||
WorkOrderCatalogItemRead,
|
||
WorkOrderCatalogRead,
|
||
WorkOrderCatalogSuggestion,
|
||
WorkOrderCorrectionCreate,
|
||
WorkOrderCorrectionRead,
|
||
WorkOrderDecision,
|
||
WorkOrderDetailRead,
|
||
WorkOrderStatusHistoryRead,
|
||
WorkOrderUpdate,
|
||
)
|
||
from app.services.sto_booking import create_service_notification
|
||
from app.services.work_orders import (
|
||
add_labor_item,
|
||
add_product_item,
|
||
add_status_history,
|
||
assign_work_order_number,
|
||
close_work_order,
|
||
ensure_work_order_editable,
|
||
refresh_work_order_totals,
|
||
)
|
||
|
||
router = APIRouter(prefix="/work-orders", tags=["work-orders"])
|
||
|
||
|
||
def webapp_url(path: str) -> str:
|
||
return f"{settings.effective_webapp_url.rstrip('/')}/{path.lstrip('/')}"
|
||
|
||
|
||
def work_order_webapp_url(work_order_id: int) -> str:
|
||
return webapp_url(f"work_order.html?id={work_order_id}")
|
||
|
||
|
||
def vehicle_profile_webapp_url(vehicle_id: int) -> str:
|
||
return webapp_url(f"?section=carProfile&car_id={vehicle_id}")
|
||
|
||
|
||
async def get_work_order(session: AsyncSession, work_order_id: int) -> ServiceVisit:
|
||
visit = await session.get(ServiceVisit, work_order_id)
|
||
if visit is None:
|
||
raise HTTPException(status_code=404, detail="Work order not found")
|
||
return visit
|
||
|
||
|
||
async def get_work_order_with_items(session: AsyncSession, work_order_id: int) -> ServiceVisit:
|
||
visit = (
|
||
await session.execute(
|
||
select(ServiceVisit)
|
||
.options(
|
||
selectinload(ServiceVisit.work_items),
|
||
selectinload(ServiceVisit.product_items),
|
||
)
|
||
.where(ServiceVisit.id == work_order_id)
|
||
)
|
||
).scalar_one_or_none()
|
||
if visit is None:
|
||
raise HTTPException(status_code=404, detail="Work order not found")
|
||
return visit
|
||
|
||
|
||
async def ensure_work_order_sto_access(
|
||
session: AsyncSession, visit: ServiceVisit, user: User, allowed_roles: set[str] | None = None
|
||
) -> None:
|
||
await ensure_service_employee(session, visit.service_center_id, user, allowed_roles)
|
||
await ensure_work_order_vehicle_scope(session, visit)
|
||
|
||
|
||
async def ensure_work_order_owner_access(session: AsyncSession, visit: ServiceVisit, user: User) -> Car:
|
||
vehicle = await session.get(Car, visit.vehicle_id)
|
||
if vehicle is None:
|
||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||
if vehicle.owner_id != user.id:
|
||
raise HTTPException(status_code=403, detail="Forbidden")
|
||
return vehicle
|
||
|
||
|
||
async def ensure_work_order_vehicle_scope(session: AsyncSession, visit: ServiceVisit) -> None:
|
||
link = (
|
||
await session.execute(
|
||
select(CarServiceLink).where(
|
||
CarServiceLink.car_id == visit.vehicle_id,
|
||
CarServiceLink.service_center_id == visit.service_center_id,
|
||
CarServiceLink.status == "approved",
|
||
CarServiceLink.is_active.is_(True),
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
if link is not None:
|
||
return
|
||
appointment = (
|
||
await session.execute(
|
||
select(ServiceAppointment).where(
|
||
ServiceAppointment.linked_work_order_id == visit.id,
|
||
ServiceAppointment.service_center_id == visit.service_center_id,
|
||
ServiceAppointment.vehicle_id == visit.vehicle_id,
|
||
ServiceAppointment.status.in_(["converted_to_work_order", "completed"]),
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
if appointment is None:
|
||
raise HTTPException(status_code=403, detail="Vehicle access is not confirmed by owner")
|
||
|
||
|
||
async def ensure_center_vehicle_scope(session: AsyncSession, service_center_id: int, vehicle_id: int) -> None:
|
||
link = (
|
||
await session.execute(
|
||
select(CarServiceLink).where(
|
||
CarServiceLink.car_id == vehicle_id,
|
||
CarServiceLink.service_center_id == service_center_id,
|
||
CarServiceLink.status == "approved",
|
||
CarServiceLink.is_active.is_(True),
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
if link is not None:
|
||
return
|
||
appointment = (
|
||
await session.execute(
|
||
select(ServiceAppointment).where(
|
||
ServiceAppointment.service_center_id == service_center_id,
|
||
ServiceAppointment.vehicle_id == vehicle_id,
|
||
ServiceAppointment.status.in_(["confirmed", "confirmed_by_sto", "converted_to_work_order", "completed"]),
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
if appointment is None:
|
||
raise HTTPException(status_code=403, detail="Vehicle access is not confirmed by owner")
|
||
|
||
|
||
def vehicle_catalog_suggestions(vehicle: Car | None) -> tuple[list[WorkOrderCatalogSuggestion], list[str]]:
|
||
if vehicle is None:
|
||
return [], []
|
||
suggestions: list[WorkOrderCatalogSuggestion] = []
|
||
missing: list[str] = []
|
||
if vehicle.engine_oil_type:
|
||
suggestions.append(
|
||
WorkOrderCatalogSuggestion(
|
||
title=f"Моторное масло {vehicle.engine_oil_type}",
|
||
category="engine_oil",
|
||
product_type="fluid",
|
||
unit="l",
|
||
default_quantity=vehicle.engine_oil_volume_l or 1,
|
||
volume=vehicle.engine_oil_volume_l,
|
||
specification=vehicle.engine_oil_type,
|
||
metadata_json={"source_field": "engine_oil_type", "vehicle_id": vehicle.id},
|
||
)
|
||
)
|
||
else:
|
||
missing.append("engine_oil")
|
||
if vehicle.transmission_fluid_type:
|
||
suggestions.append(
|
||
WorkOrderCatalogSuggestion(
|
||
title=f"Трансмиссионная жидкость {vehicle.transmission_fluid_type}",
|
||
category="transmission_fluid",
|
||
product_type="fluid",
|
||
unit="l",
|
||
default_quantity=vehicle.transmission_fluid_volume_l or 1,
|
||
volume=vehicle.transmission_fluid_volume_l,
|
||
specification=vehicle.transmission_fluid_type,
|
||
metadata_json={"source_field": "transmission_fluid_type", "vehicle_id": vehicle.id},
|
||
)
|
||
)
|
||
else:
|
||
missing.append("transmission_fluid")
|
||
if vehicle.coolant_type:
|
||
suggestions.append(
|
||
WorkOrderCatalogSuggestion(
|
||
title=f"Антифриз {vehicle.coolant_type}",
|
||
category="coolant",
|
||
product_type="fluid",
|
||
unit="l",
|
||
specification=vehicle.coolant_type,
|
||
metadata_json={"source_field": "coolant_type", "vehicle_id": vehicle.id},
|
||
)
|
||
)
|
||
else:
|
||
missing.append("coolant")
|
||
if vehicle.brake_fluid_type:
|
||
suggestions.append(
|
||
WorkOrderCatalogSuggestion(
|
||
title=f"Тормозная жидкость {vehicle.brake_fluid_type}",
|
||
category="brake_fluid",
|
||
product_type="fluid",
|
||
unit="l",
|
||
specification=vehicle.brake_fluid_type,
|
||
metadata_json={"source_field": "brake_fluid_type", "vehicle_id": vehicle.id},
|
||
)
|
||
)
|
||
else:
|
||
missing.append("brake_fluid")
|
||
return suggestions, missing
|
||
|
||
|
||
async def load_catalog(
|
||
session: AsyncSession,
|
||
*,
|
||
service_center_id: int | None = None,
|
||
vehicle_id: int | None = None,
|
||
item_type: str | None = None,
|
||
) -> WorkOrderCatalogRead:
|
||
stmt = select(WorkOrderCatalogItem).where(WorkOrderCatalogItem.is_active.is_(True))
|
||
if service_center_id is not None:
|
||
stmt = stmt.where(
|
||
or_(
|
||
WorkOrderCatalogItem.service_center_id.is_(None),
|
||
WorkOrderCatalogItem.service_center_id == service_center_id,
|
||
)
|
||
)
|
||
else:
|
||
stmt = stmt.where(WorkOrderCatalogItem.service_center_id.is_(None))
|
||
if item_type:
|
||
stmt = stmt.where(WorkOrderCatalogItem.item_type == item_type)
|
||
stmt = stmt.order_by(WorkOrderCatalogItem.service_center_id.is_(None), WorkOrderCatalogItem.title.asc())
|
||
items = list((await session.execute(stmt)).scalars())
|
||
vehicle = await session.get(Car, vehicle_id) if vehicle_id else None
|
||
suggestions, missing = vehicle_catalog_suggestions(vehicle)
|
||
if item_type:
|
||
suggestions = [item for item in suggestions if item.item_type == item_type]
|
||
return WorkOrderCatalogRead(items=items, vehicle_suggestions=suggestions, missing_vehicle_fields=missing)
|
||
|
||
|
||
@router.get("/catalog", response_model=WorkOrderCatalogRead)
|
||
async def list_work_order_catalog(
|
||
service_center_id: int | None = None,
|
||
vehicle_id: int | None = None,
|
||
item_type: str | None = Query(default=None, pattern="^(work|product)$"),
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> WorkOrderCatalogRead:
|
||
if service_center_id is not None:
|
||
await ensure_service_employee(session, service_center_id, current_user, {"owner", "manager", "receptionist", "mechanic"})
|
||
if service_center_id is not None and vehicle_id is not None:
|
||
await ensure_center_vehicle_scope(session, service_center_id, vehicle_id)
|
||
return await load_catalog(session, service_center_id=service_center_id, vehicle_id=vehicle_id, item_type=item_type)
|
||
|
||
|
||
@router.post("/catalog", response_model=WorkOrderCatalogItemRead, status_code=status.HTTP_201_CREATED)
|
||
async def create_work_order_catalog_item(
|
||
payload: WorkOrderCatalogItemCreate,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> WorkOrderCatalogItem:
|
||
if payload.service_center_id is None:
|
||
raise HTTPException(status_code=400, detail="Service center catalog item must have service_center_id")
|
||
await ensure_service_employee(session, payload.service_center_id, current_user, {"owner", "manager"})
|
||
item = WorkOrderCatalogItem(**payload.model_dump())
|
||
session.add(item)
|
||
await log_audit(session, actor=current_user, action="work_order_catalog.create", target_type="service_center", target_id=payload.service_center_id)
|
||
await session.commit()
|
||
await session.refresh(item)
|
||
return item
|
||
|
||
|
||
@router.get("/{work_order_id}", response_model=ServiceVisitRead)
|
||
async def get_work_order_detail(
|
||
work_order_id: int,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
vehicle = await session.get(Car, visit.vehicle_id)
|
||
if vehicle is None:
|
||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||
if vehicle.owner_id == current_user.id:
|
||
return visit
|
||
await ensure_work_order_sto_access(session, visit, current_user)
|
||
return visit
|
||
|
||
|
||
@router.get("/{work_order_id}/detail", response_model=WorkOrderDetailRead)
|
||
async def get_work_order_rich_detail(
|
||
work_order_id: int,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> WorkOrderDetailRead:
|
||
visit = await get_work_order_with_items(session, work_order_id)
|
||
vehicle = await session.get(Car, visit.vehicle_id)
|
||
if vehicle is None:
|
||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||
if vehicle.owner_id != current_user.id:
|
||
await ensure_work_order_sto_access(session, visit, current_user)
|
||
center = await session.get(ServiceCenter, visit.service_center_id)
|
||
if center is None:
|
||
raise HTTPException(status_code=404, detail="Service center not found")
|
||
return WorkOrderDetailRead(
|
||
visit=visit,
|
||
vehicle=vehicle,
|
||
service_center=center,
|
||
work_items=list(visit.work_items),
|
||
product_items=list(visit.product_items),
|
||
catalog=await load_catalog(session, service_center_id=visit.service_center_id, vehicle_id=visit.vehicle_id),
|
||
)
|
||
|
||
|
||
@router.patch("/{work_order_id}", response_model=ServiceVisitRead)
|
||
async def update_work_order(
|
||
work_order_id: int,
|
||
payload: WorkOrderUpdate,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "receptionist", "mechanic"})
|
||
await ensure_work_order_editable(visit)
|
||
for field, value in payload.model_dump(exclude_unset=True).items():
|
||
setattr(visit, field, value)
|
||
await refresh_work_order_totals(session, visit)
|
||
await log_audit(session, actor=current_user, action="work_order.update", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(visit)
|
||
return visit
|
||
|
||
|
||
@router.post("/{work_order_id}/labor-items", response_model=ServiceWorkItemRead, status_code=status.HTTP_201_CREATED)
|
||
async def create_labor_item(
|
||
work_order_id: int,
|
||
payload: ServiceWorkItemCreate,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceWorkItem:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "mechanic"})
|
||
item = await add_labor_item(session, visit, payload=payload.model_dump())
|
||
await log_audit(session, actor=current_user, action="work_order.labor_item.create", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(item)
|
||
return item
|
||
|
||
|
||
@router.post("/{work_order_id}/product-items", response_model=ServiceProductItemRead, status_code=status.HTTP_201_CREATED)
|
||
async def create_product_item(
|
||
work_order_id: int,
|
||
payload: ServiceProductItemCreate,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceProductItem:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "mechanic"})
|
||
item = await add_product_item(session, visit, payload=payload.model_dump())
|
||
await log_audit(session, actor=current_user, action="work_order.product_item.create", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(item)
|
||
return item
|
||
|
||
|
||
@router.post("/{work_order_id}/submit-approval", response_model=ServiceVisitRead)
|
||
async def submit_work_order_for_approval(
|
||
work_order_id: int,
|
||
payload: WorkOrderDecision,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "receptionist"})
|
||
await ensure_work_order_editable(visit)
|
||
await assign_work_order_number(session, visit)
|
||
await refresh_work_order_totals(session, visit)
|
||
visit.approval_required = True
|
||
await add_status_history(session, visit, to_status="waiting_owner_approval", actor=current_user, comment=payload.comment)
|
||
vehicle = await session.get(Car, visit.vehicle_id)
|
||
if vehicle is None:
|
||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||
await create_service_notification(
|
||
session,
|
||
recipient_user_id=vehicle.owner_id,
|
||
service_center_id=visit.service_center_id,
|
||
notification_type="work_order.waiting_owner_approval",
|
||
title="Заказ-наряд ожидает согласования",
|
||
body=f"{visit.work_order_number}: {visit.final_total} {visit.currency}. Откройте детали, чтобы согласовать или отклонить смету.",
|
||
idempotency_key=f"work_order:{visit.id}:waiting_owner_approval:{visit.final_total}",
|
||
web_app_url=work_order_webapp_url(visit.id),
|
||
button_text="Согласовать заказ-наряд",
|
||
)
|
||
await log_audit(session, actor=current_user, action="work_order.submit_approval", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(visit)
|
||
return visit
|
||
|
||
|
||
@router.post("/{work_order_id}/approve", response_model=ServiceVisitRead)
|
||
async def approve_work_order(
|
||
work_order_id: int,
|
||
payload: WorkOrderDecision,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_owner_access(session, visit, current_user)
|
||
if visit.status != "waiting_owner_approval":
|
||
raise HTTPException(status_code=409, detail="Work order is not waiting for owner approval")
|
||
await refresh_work_order_totals(session, visit)
|
||
visit.price_approved_total = visit.final_total
|
||
visit.approved_at = datetime.now(UTC)
|
||
visit.owner_resolved_at = visit.approved_at
|
||
visit.owner_comment = payload.comment
|
||
await add_status_history(session, visit, to_status="approved_by_owner", actor=current_user, comment=payload.comment)
|
||
await create_service_notification(
|
||
session,
|
||
recipient_user_id=visit.owner_id or current_user.id,
|
||
service_center_id=visit.service_center_id,
|
||
notification_type="work_order.approved_by_owner",
|
||
title="Заказ-наряд согласован",
|
||
body=visit.work_order_number,
|
||
send_telegram=False,
|
||
idempotency_key=f"work_order:{visit.id}:approved_by_owner",
|
||
)
|
||
await log_audit(session, actor=current_user, action="work_order.approve", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(visit)
|
||
return visit
|
||
|
||
|
||
@router.post("/{work_order_id}/reject", response_model=ServiceVisitRead)
|
||
async def reject_work_order(
|
||
work_order_id: int,
|
||
payload: WorkOrderDecision,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_owner_access(session, visit, current_user)
|
||
if visit.status != "waiting_owner_approval":
|
||
raise HTTPException(status_code=409, detail="Work order is not waiting for owner approval")
|
||
visit.owner_comment = payload.comment
|
||
visit.owner_resolved_at = datetime.now(UTC)
|
||
await add_status_history(session, visit, to_status="rejected_by_owner", actor=current_user, comment=payload.comment)
|
||
await log_audit(session, actor=current_user, action="work_order.reject", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(visit)
|
||
return visit
|
||
|
||
|
||
@router.post("/{work_order_id}/start", response_model=ServiceVisitRead)
|
||
async def start_work_order(
|
||
work_order_id: int,
|
||
payload: WorkOrderDecision,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "mechanic"})
|
||
if visit.status not in {"draft", "diagnosis", "approved_by_owner"}:
|
||
raise HTTPException(status_code=409, detail="Work order cannot be started")
|
||
await add_status_history(session, visit, to_status="in_progress", actor=current_user, comment=payload.comment)
|
||
await log_audit(session, actor=current_user, action="work_order.start", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(visit)
|
||
return visit
|
||
|
||
|
||
@router.post("/{work_order_id}/complete", response_model=ServiceVisitRead)
|
||
async def complete_work_order(
|
||
work_order_id: int,
|
||
payload: WorkOrderDecision,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager"})
|
||
await close_work_order(
|
||
session,
|
||
visit,
|
||
actor=current_user,
|
||
confirm_lower_odometer=payload.confirm_lower_odometer,
|
||
)
|
||
await log_audit(session, actor=current_user, action="work_order.complete", target_type="service_visit", target_id=visit.id)
|
||
await session.commit()
|
||
await session.refresh(visit)
|
||
return visit
|
||
|
||
|
||
@router.post("/{work_order_id}/request-vehicle-profile", response_model=ServiceVisitRead)
|
||
async def request_vehicle_profile_details(
|
||
work_order_id: int,
|
||
payload: VehicleProfileRequest,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> ServiceVisit:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager", "receptionist", "mechanic"})
|
||
vehicle = await session.get(Car, visit.vehicle_id)
|
||
if vehicle is None:
|
||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||
missing = payload.missing_fields or vehicle_catalog_suggestions(vehicle)[1]
|
||
missing_text = ", ".join(missing) if missing else "масло и технические жидкости"
|
||
await create_service_notification(
|
||
session,
|
||
recipient_user_id=vehicle.owner_id,
|
||
service_center_id=visit.service_center_id,
|
||
notification_type="vehicle_profile.requested_by_sto",
|
||
title="СТО просит заполнить карточку авто",
|
||
body=payload.comment or f"Для точного подбора материалов укажите данные: {missing_text}.",
|
||
idempotency_key=f"work_order:{visit.id}:vehicle_profile_request:{','.join(sorted(missing))}",
|
||
web_app_url=vehicle_profile_webapp_url(vehicle.id),
|
||
button_text="Заполнить карточку авто",
|
||
)
|
||
await log_audit(
|
||
session,
|
||
actor=current_user,
|
||
action="work_order.vehicle_profile.request",
|
||
target_type="service_visit",
|
||
target_id=visit.id,
|
||
metadata={"missing_fields": missing},
|
||
)
|
||
await session.commit()
|
||
await session.refresh(visit)
|
||
return visit
|
||
|
||
|
||
@router.post("/{work_order_id}/corrections", response_model=WorkOrderCorrectionRead, status_code=status.HTTP_201_CREATED)
|
||
async def create_work_order_correction(
|
||
work_order_id: int,
|
||
payload: WorkOrderCorrectionCreate,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> WorkOrderCorrection:
|
||
visit = await get_work_order(session, work_order_id)
|
||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager"})
|
||
if visit.status != "completed":
|
||
raise HTTPException(status_code=409, detail="Correction flow is only for completed work orders")
|
||
correction = WorkOrderCorrection(
|
||
service_visit_id=visit.id,
|
||
requested_by_user_id=current_user.id,
|
||
reason=payload.reason,
|
||
proposed_changes=payload.proposed_changes,
|
||
owner_approval_required=payload.owner_approval_required,
|
||
created_version=visit.version or 1,
|
||
)
|
||
session.add(correction)
|
||
await log_audit(
|
||
session,
|
||
actor=current_user,
|
||
action="work_order.correction.create",
|
||
target_type="service_visit",
|
||
target_id=visit.id,
|
||
metadata={"reason": payload.reason},
|
||
)
|
||
await session.commit()
|
||
await session.refresh(correction)
|
||
return correction
|
||
|
||
|
||
@router.get("/{work_order_id}/status-history", response_model=list[WorkOrderStatusHistoryRead])
|
||
async def work_order_status_history(
|
||
work_order_id: int,
|
||
session: AsyncSession = Depends(get_session),
|
||
current_user: User = Depends(get_current_telegram_user),
|
||
) -> list[WorkOrderStatusHistory]:
|
||
visit = await get_work_order(session, work_order_id)
|
||
vehicle = await session.get(Car, visit.vehicle_id)
|
||
if vehicle is None:
|
||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||
if vehicle.owner_id != current_user.id:
|
||
await ensure_work_order_sto_access(session, visit, current_user)
|
||
result = await session.execute(
|
||
select(WorkOrderStatusHistory)
|
||
.where(WorkOrderStatusHistory.service_visit_id == visit.id)
|
||
.order_by(WorkOrderStatusHistory.created_at.asc(), WorkOrderStatusHistory.id.asc())
|
||
)
|
||
return list(result.scalars())
|