This commit is contained in:
@@ -93,6 +93,23 @@ async def _appointment_for_sto(session: AsyncSession, appointment_id: int, user:
|
||||
return appointment
|
||||
|
||||
|
||||
def _ensure_appointment_deletable(appointment: ServiceAppointment) -> None:
|
||||
if appointment.linked_work_order_id or appointment.status in {"converted_to_work_order", "completed"}:
|
||||
raise HTTPException(status_code=409, detail="Appointment already has a work order and cannot be deleted")
|
||||
|
||||
|
||||
async def _restore_recommendation_after_appointment_delete(
|
||||
session: AsyncSession,
|
||||
appointment: ServiceAppointment,
|
||||
) -> None:
|
||||
if not appointment.source_recommendation_id:
|
||||
return
|
||||
recommendation = await session.get(MaintenanceRecommendation, appointment.source_recommendation_id)
|
||||
if recommendation is not None and recommendation.status == "booked":
|
||||
recommendation.status = "active"
|
||||
recommendation.source_appointment_id = None
|
||||
|
||||
|
||||
@router.get("/sto/catalog", response_model=list[ServiceCatalogItem])
|
||||
async def get_sto_catalog(
|
||||
city: str | None = None,
|
||||
@@ -276,6 +293,34 @@ async def cancel_appointment(
|
||||
return appointment
|
||||
|
||||
|
||||
@router.delete("/appointments/{appointment_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_appointment_by_owner(
|
||||
appointment_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> None:
|
||||
appointment = await _appointment_for_owner(session, appointment_id, current_user)
|
||||
_ensure_appointment_deletable(appointment)
|
||||
await _restore_recommendation_after_appointment_delete(session, appointment)
|
||||
await notify_service_staff(
|
||||
session,
|
||||
service_center_id=appointment.service_center_id,
|
||||
notification_type="appointment.deleted_by_owner",
|
||||
title="Клиент удалил запись",
|
||||
body=f"{appointment.service_name}: {appointment.requested_start_at:%Y-%m-%d %H:%M}",
|
||||
appointment_id=None,
|
||||
)
|
||||
await log_audit(
|
||||
session,
|
||||
actor=current_user,
|
||||
action="appointment.delete_by_owner",
|
||||
target_type="service_appointment",
|
||||
target_id=appointment_id,
|
||||
)
|
||||
await session.delete(appointment)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@router.post("/appointments/{appointment_id}/accept-proposed-time", response_model=AppointmentRead)
|
||||
async def accept_proposed_time(
|
||||
appointment_id: int,
|
||||
@@ -515,6 +560,36 @@ async def reject_appointment(
|
||||
return appointment
|
||||
|
||||
|
||||
@router.delete("/sto/appointments/{appointment_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_appointment_by_sto(
|
||||
appointment_id: int,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
current_user: User = Depends(get_current_telegram_user),
|
||||
) -> None:
|
||||
appointment = await _appointment_for_sto(session, appointment_id, current_user)
|
||||
_ensure_appointment_deletable(appointment)
|
||||
await _restore_recommendation_after_appointment_delete(session, appointment)
|
||||
await create_service_notification(
|
||||
session,
|
||||
recipient_user_id=appointment.owner_id,
|
||||
service_center_id=appointment.service_center_id,
|
||||
appointment_id=None,
|
||||
notification_type="appointment.deleted_by_sto",
|
||||
title="СТО удалило запись",
|
||||
body=f"{appointment.service_name}: {appointment.requested_start_at:%Y-%m-%d %H:%M}",
|
||||
idempotency_key=f"appointment:{appointment.id}:deleted_by_sto",
|
||||
)
|
||||
await log_audit(
|
||||
session,
|
||||
actor=current_user,
|
||||
action="appointment.delete_by_sto",
|
||||
target_type="service_appointment",
|
||||
target_id=appointment_id,
|
||||
)
|
||||
await session.delete(appointment)
|
||||
await session.commit()
|
||||
|
||||
|
||||
@router.post("/sto/appointments/{appointment_id}/propose-time", response_model=AppointmentRead)
|
||||
async def propose_appointment_time(
|
||||
appointment_id: int,
|
||||
@@ -570,6 +645,9 @@ async def create_work_order_from_appointment(
|
||||
if visit is None:
|
||||
raise HTTPException(status_code=404, detail="Linked work order not found")
|
||||
return visit
|
||||
vehicle = await session.get(Car, appointment.vehicle_id)
|
||||
if vehicle is None:
|
||||
raise HTTPException(status_code=404, detail="Vehicle not found")
|
||||
visit = ServiceVisit(
|
||||
service_center_id=appointment.service_center_id,
|
||||
vehicle_id=appointment.vehicle_id,
|
||||
@@ -577,7 +655,7 @@ async def create_work_order_from_appointment(
|
||||
created_by_employee_id=employee.id,
|
||||
assigned_employee_id=employee.id,
|
||||
visit_date=(appointment.confirmed_start_at or appointment.requested_start_at).date(),
|
||||
odometer=payload.odometer,
|
||||
odometer=payload.odometer if payload.odometer is not None else vehicle.current_odometer,
|
||||
status="draft",
|
||||
customer_complaint=appointment.customer_comment,
|
||||
notes=payload.notes or appointment.customer_comment,
|
||||
|
||||
@@ -50,6 +50,51 @@ def line_total(quantity: Decimal, unit_price: Decimal | None, discount: Decimal)
|
||||
return max(Decimal("0"), Decimal(quantity) * money(unit_price) - money(discount)).quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
def _product_profile_label(product: ServiceProductItem) -> str | None:
|
||||
details = [product.viscosity, product.specification]
|
||||
label = " ".join(str(item).strip() for item in details if item).strip()
|
||||
return label or product.specification or product.title or None
|
||||
|
||||
|
||||
def _product_volume(product: ServiceProductItem) -> Decimal | None:
|
||||
if product.used_volume is not None:
|
||||
return product.used_volume
|
||||
if product.volume is not None:
|
||||
return product.volume
|
||||
if product.unit == "l":
|
||||
return product.quantity
|
||||
return None
|
||||
|
||||
|
||||
def sync_vehicle_profile_from_products(vehicle: Car, product_items: list[ServiceProductItem]) -> dict[str, str]:
|
||||
updates: dict[str, str] = {}
|
||||
for product in product_items:
|
||||
category = product.category
|
||||
label = _product_profile_label(product)
|
||||
volume = _product_volume(product)
|
||||
if category == "engine_oil":
|
||||
if label and not vehicle.engine_oil_type:
|
||||
vehicle.engine_oil_type = label
|
||||
updates["engine_oil_type"] = label
|
||||
if volume is not None and vehicle.engine_oil_volume_l is None:
|
||||
vehicle.engine_oil_volume_l = volume
|
||||
updates["engine_oil_volume_l"] = str(volume)
|
||||
elif category == "transmission_fluid":
|
||||
if label and not vehicle.transmission_fluid_type:
|
||||
vehicle.transmission_fluid_type = label
|
||||
updates["transmission_fluid_type"] = label
|
||||
if volume is not None and vehicle.transmission_fluid_volume_l is None:
|
||||
vehicle.transmission_fluid_volume_l = volume
|
||||
updates["transmission_fluid_volume_l"] = str(volume)
|
||||
elif category == "coolant" and label and not vehicle.coolant_type:
|
||||
vehicle.coolant_type = label
|
||||
updates["coolant_type"] = label
|
||||
elif category == "brake_fluid" and label and not vehicle.brake_fluid_type:
|
||||
vehicle.brake_fluid_type = label
|
||||
updates["brake_fluid_type"] = label
|
||||
return updates
|
||||
|
||||
|
||||
async def add_status_history(
|
||||
session: AsyncSession,
|
||||
visit: ServiceVisit,
|
||||
@@ -294,6 +339,7 @@ async def close_work_order(
|
||||
source_appointment_id=appointment.id if appointment else None,
|
||||
)
|
||||
)
|
||||
vehicle_profile_updates = sync_vehicle_profile_from_products(vehicle, product_items)
|
||||
visit.version = (visit.version or 1) + 1
|
||||
visit.completed_snapshot = {
|
||||
"work_order_number": visit.work_order_number,
|
||||
@@ -306,6 +352,7 @@ async def close_work_order(
|
||||
"final_total": str(visit.final_total),
|
||||
"currency": visit.currency,
|
||||
"completed_at": visit.completed_at.isoformat() if visit.completed_at else None,
|
||||
"vehicle_profile_updates": vehicle_profile_updates,
|
||||
}
|
||||
await create_service_notification(
|
||||
session,
|
||||
|
||||
Reference in New Issue
Block a user