Sync completed work orders into vehicle records
Some checks failed
ci / test (push) Has been cancelled
Some checks failed
ci / test (push) Has been cancelled
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import ensure_service_employee, get_current_telegram_user, log_audit
|
||||
from app.db.session import get_session
|
||||
from app.models.car import Car, ServiceVisit, ServiceWorkItem, VehicleDataChangeRequest
|
||||
from app.models.expense import ExpenseCategory, ExpenseEntry, ServiceEntry, ServiceType
|
||||
from app.models.user import User
|
||||
from app.schemas.service_center import (
|
||||
ServiceVisitRead,
|
||||
@@ -27,6 +30,51 @@ async def get_visit_or_404(session: AsyncSession, visit_id: int) -> ServiceVisit
|
||||
return visit
|
||||
|
||||
|
||||
async def ensure_visit_owner_records(session: AsyncSession, visit: ServiceVisit, vehicle: Car) -> None:
|
||||
service = (
|
||||
await session.execute(
|
||||
select(ServiceEntry).where(ServiceEntry.service_visit_id == visit.id)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
expense = (
|
||||
await session.execute(
|
||||
select(ExpenseEntry).where(ExpenseEntry.service_visit_id == visit.id)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if service is not None and expense is not None:
|
||||
return
|
||||
amount = Decimal(str(visit.final_total or visit.total_cost or 0)).quantize(Decimal("0.01"))
|
||||
title = f"Визит СТО #{visit.id}"
|
||||
if service is None:
|
||||
session.add(
|
||||
ServiceEntry(
|
||||
car_id=vehicle.id,
|
||||
service_visit_id=visit.id,
|
||||
entry_date=visit.visit_date,
|
||||
odometer=visit.odometer,
|
||||
service_type=ServiceType.maintenance,
|
||||
title=title,
|
||||
category="service_visit",
|
||||
total_cost=amount,
|
||||
notes=visit.service_comment or visit.notes,
|
||||
)
|
||||
)
|
||||
if expense is None:
|
||||
session.add(
|
||||
ExpenseEntry(
|
||||
car_id=vehicle.id,
|
||||
service_visit_id=visit.id,
|
||||
entry_date=visit.visit_date,
|
||||
category=ExpenseCategory.maintenance,
|
||||
title=title,
|
||||
total_cost=max(amount, Decimal("0")),
|
||||
currency=visit.currency,
|
||||
odometer=visit.odometer,
|
||||
metadata_json={"service_visit_id": visit.id},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{visit_id}/work-items", response_model=ServiceWorkItemRead, status_code=status.HTTP_201_CREATED)
|
||||
async def add_work_item(
|
||||
visit_id: int,
|
||||
@@ -84,6 +132,7 @@ async def confirm_visit(
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
visit.status = "confirmed"
|
||||
visit.owner_resolved_at = datetime.now(UTC)
|
||||
await ensure_visit_owner_records(session, visit, vehicle)
|
||||
await apply_odometer_from_record(
|
||||
session,
|
||||
vehicle,
|
||||
|
||||
@@ -487,6 +487,8 @@ async def complete_work_order(
|
||||
) -> ServiceVisit:
|
||||
visit = await get_work_order(session, work_order_id)
|
||||
await ensure_work_order_sto_access(session, visit, current_user, {"owner", "manager"})
|
||||
if payload.odometer is not None:
|
||||
visit.odometer = payload.odometer
|
||||
await close_work_order(
|
||||
session,
|
||||
visit,
|
||||
|
||||
@@ -489,6 +489,7 @@ class WorkOrderUpdate(BaseModel):
|
||||
|
||||
class WorkOrderDecision(BaseModel):
|
||||
comment: str | None = None
|
||||
odometer: int | None = None
|
||||
confirm_lower_odometer: bool = False
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,16 @@ async def test_service_visit_owner_confirmation_and_change_request(
|
||||
assert complete_response.json()["status"] == "pending_owner_confirmation"
|
||||
assert confirm_response.json()["status"] == "confirmed"
|
||||
assert approve_response.json()["status"] == "approved"
|
||||
refreshed = await client.get(f"/api/cars/{vehicle['id']}", headers=auth_headers)
|
||||
services = await client.get(f"/api/cars/{vehicle['id']}/service", headers=auth_headers)
|
||||
stats = await client.get(
|
||||
f"/api/cars/{vehicle['id']}/stats?date_from=2026-05-01&date_to=2026-05-31",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert refreshed.json()["current_odometer"] == 12345
|
||||
assert services.json()[0]["total_cost"] == "100.00"
|
||||
assert stats.json()["service_cost"] == "100.00"
|
||||
assert stats.json()["total_cost"] == "100.00"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -169,7 +169,7 @@ async def test_work_order_completion_creates_vehicle_records_and_updates_costs(
|
||||
completed = await client.post(
|
||||
f"/api/work-orders/{work_order['id']}/complete",
|
||||
headers=auth_headers,
|
||||
json={},
|
||||
json={"odometer": 10300},
|
||||
)
|
||||
assert completed.status_code == 200
|
||||
assert completed.json()["status"] == "completed"
|
||||
@@ -210,7 +210,7 @@ async def test_work_order_completion_creates_vehicle_records_and_updates_costs(
|
||||
assert sum(1 for item in service_history.json()["service_visits"] if item["id"] == work_order["id"]) == 1
|
||||
assert len(expenses.json()) == 1
|
||||
assert expenses.json()[0]["total_cost"] == "130.00"
|
||||
assert refreshed.json()["current_odometer"] == 10150
|
||||
assert refreshed.json()["current_odometer"] == 10300
|
||||
assert refreshed.json()["engine_oil_type"] == "5W-30"
|
||||
assert refreshed.json()["engine_oil_volume_l"] == "4.00"
|
||||
assert stats.json()["total_cost"] == "130.00"
|
||||
|
||||
@@ -1658,9 +1658,10 @@ function bindMechanicWorkplaceActions(root) {
|
||||
});
|
||||
root.querySelectorAll("[data-complete-work-order]").forEach((button) => {
|
||||
button.addEventListener("click", () => runAction(button, "Завершаю заказ-наряд...", async () => {
|
||||
const odometerValue = window.prompt("Пробег на закрытии, км. Можно оставить пустым, если пробег уже указан.") || "";
|
||||
await api(`/work-orders/${button.dataset.completeWorkOrder}/complete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ comment: "Работы завершены" }),
|
||||
body: JSON.stringify({ comment: "Работы завершены", odometer: numberOrNull(odometerValue) }),
|
||||
});
|
||||
await loadMechanicWorkplace();
|
||||
}));
|
||||
|
||||
@@ -445,9 +445,10 @@ document.body.addEventListener("click", async (event) => {
|
||||
}));
|
||||
}
|
||||
if (button.dataset.completeWorkOrder) {
|
||||
const odometer = window.prompt("Пробег на закрытии, км. Можно оставить пустым, если пробег уже указан.") || "";
|
||||
await runAction(button, () => api(`/work-orders/${button.dataset.completeWorkOrder}/complete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ comment: "Работы завершены" }),
|
||||
body: JSON.stringify({ comment: "Работы завершены", odometer: numberOrNull(odometer) }),
|
||||
}));
|
||||
}
|
||||
if (button.dataset.requestVehicleProfile) {
|
||||
|
||||
Reference in New Issue
Block a user