szolgáltatók beálltásai, szerkesztése , létrehozása
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/assets.py
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -11,12 +11,12 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.api.deps import get_current_user
|
||||
from app.models import Asset, AssetCost
|
||||
from app.models import Asset, AssetCatalog, AssetCost, AssetEvent, OdometerReading, CostCategory, OrganizationMember
|
||||
from app.models.identity import User
|
||||
from app.services.cost_service import cost_service
|
||||
from app.services.asset_service import AssetService
|
||||
from app.schemas.asset_cost import AssetCostCreate, AssetCostResponse
|
||||
from app.schemas.asset import AssetResponse, AssetCreate, AssetUpdate
|
||||
from app.schemas.asset import AssetResponse, AssetCreate, AssetUpdate, AssetEventCreate, AssetEventResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -174,7 +174,10 @@ async def get_user_vehicles(
|
||||
.order_by(order_expr, Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
.options(
|
||||
selectinload(Asset.catalog),
|
||||
selectinload(Asset.odometer_readings)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Corporate mode: only show vehicles belonging to the active organization's garages
|
||||
@@ -212,7 +215,10 @@ async def get_user_vehicles(
|
||||
.order_by(order_expr, Asset.created_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.options(selectinload(Asset.catalog))
|
||||
.options(
|
||||
selectinload(Asset.catalog),
|
||||
selectinload(Asset.odometer_readings)
|
||||
)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
@@ -294,9 +300,6 @@ async def get_asset(
|
||||
and vehicle model definition specifications.
|
||||
"""
|
||||
# Check if user has access to this asset
|
||||
from sqlalchemy import or_
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
|
||||
# Get user's organization memberships
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
@@ -317,7 +320,8 @@ async def get_asset(
|
||||
)
|
||||
)
|
||||
.options(
|
||||
selectinload(Asset.catalog).selectinload(AssetCatalog.master_definition)
|
||||
selectinload(Asset.catalog).selectinload(AssetCatalog.master_definition),
|
||||
selectinload(Asset.odometer_readings)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -427,7 +431,7 @@ async def create_or_claim_vehicle(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Vehicle creation error: {e}")
|
||||
logger.error(f"Vehicle creation failed: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű létrehozásakor")
|
||||
|
||||
|
||||
@@ -508,7 +512,7 @@ async def update_vehicle(
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Vehicle update error: {e}")
|
||||
logger.error(f"Vehicle update error: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail="Belső szerverhiba a jármű frissítésekor")
|
||||
|
||||
|
||||
@@ -588,11 +592,7 @@ async def create_maintenance_record(
|
||||
- currency: string (optional, default: "EUR")
|
||||
- invoice_number: string (optional)
|
||||
"""
|
||||
# Check if user has access to this asset
|
||||
from sqlalchemy import or_
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
|
||||
# Get user's organization memberships
|
||||
# Get user's organization memberships (imports already at module level)
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
)
|
||||
@@ -629,7 +629,6 @@ async def create_maintenance_record(
|
||||
|
||||
try:
|
||||
# Parse date
|
||||
from datetime import datetime
|
||||
date = datetime.fromisoformat(payload["date"].replace("Z", "+00:00"))
|
||||
|
||||
# Determine organization ID: use asset's current org, owner org, or user's active organization
|
||||
@@ -642,8 +641,6 @@ async def create_maintenance_record(
|
||||
organization_id = int(current_user.scope_id)
|
||||
except (ValueError, TypeError):
|
||||
# If scope_id is not a valid integer, try to get from organization memberships
|
||||
from sqlalchemy import select
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
).limit(1)
|
||||
@@ -652,8 +649,6 @@ async def create_maintenance_record(
|
||||
organization_id = org_row[0] if org_row else None
|
||||
else:
|
||||
# Try to get from organization memberships
|
||||
from sqlalchemy import select
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
).limit(1)
|
||||
@@ -667,11 +662,11 @@ async def create_maintenance_record(
|
||||
detail="Cannot determine organization for this cost record. Please ensure you have an active organization."
|
||||
)
|
||||
|
||||
# Create AssetCost record
|
||||
# Create AssetCost record with MAINTENANCE category (id=2)
|
||||
maintenance_cost = AssetCost(
|
||||
asset_id=asset_id,
|
||||
organization_id=organization_id,
|
||||
cost_category="maintenance",
|
||||
category_id=2,
|
||||
amount_net=float(payload["cost"]),
|
||||
currency=payload.get("currency", "EUR"),
|
||||
date=date,
|
||||
@@ -684,17 +679,24 @@ async def create_maintenance_record(
|
||||
)
|
||||
|
||||
db.add(maintenance_cost)
|
||||
await db.commit()
|
||||
await db.refresh(maintenance_cost)
|
||||
await db.flush() # Flush to get maintenance_cost.id
|
||||
|
||||
# Also create an AssetEvent for the maintenance
|
||||
from app.models.vehicle import AssetEvent
|
||||
# Also create an AssetEvent for the maintenance (linked via cost_id)
|
||||
maintenance_event = AssetEvent(
|
||||
asset_id=asset_id,
|
||||
event_type="maintenance"
|
||||
user_id=current_user.id,
|
||||
organization_id=organization_id,
|
||||
event_type="MAINTENANCE",
|
||||
odometer_reading=payload.get("odometer"),
|
||||
description=payload.get("description", "Maintenance record"),
|
||||
cost_id=maintenance_cost.id,
|
||||
event_date=date,
|
||||
)
|
||||
db.add(maintenance_event)
|
||||
|
||||
# Single commit for both records
|
||||
await db.commit()
|
||||
await db.refresh(maintenance_cost)
|
||||
|
||||
return maintenance_cost
|
||||
|
||||
@@ -712,6 +714,244 @@ async def create_maintenance_record(
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# DIGITÁLIS SZERVIZKÖNYV (Service Book) — AssetEvent végpontok
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
async def _check_asset_access(
|
||||
db: AsyncSession,
|
||||
asset_id: uuid.UUID,
|
||||
current_user: User
|
||||
) -> Asset:
|
||||
"""
|
||||
Helper: check if the current user has access to the given asset.
|
||||
Returns the Asset or raises 404.
|
||||
"""
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
|
||||
org_stmt = select(OrganizationMember.organization_id).where(
|
||||
OrganizationMember.user_id == current_user.id
|
||||
)
|
||||
org_result = await db.execute(org_stmt)
|
||||
user_org_ids = [row[0] for row in org_result.all()]
|
||||
|
||||
stmt = (
|
||||
select(Asset)
|
||||
.where(
|
||||
Asset.id == asset_id,
|
||||
or_(
|
||||
Asset.owner_person_id == current_user.id,
|
||||
Asset.owner_org_id.in_(user_org_ids) if user_org_ids else False,
|
||||
Asset.operator_person_id == current_user.id,
|
||||
Asset.operator_org_id.in_(user_org_ids) if user_org_ids else False
|
||||
)
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
asset = result.scalar_one_or_none()
|
||||
|
||||
if not asset:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Asset not found or you don't have permission to access it"
|
||||
)
|
||||
return asset
|
||||
|
||||
|
||||
async def _resolve_cost_category_for_event(db: AsyncSession, event_type: str) -> int:
|
||||
"""
|
||||
Helper: resolve a CostCategory ID based on the AssetEvent event_type.
|
||||
|
||||
Maps service-book event types to cost categories:
|
||||
- SERVICE, MAINTENANCE, INSPECTION, TIRE_CHANGE -> 'MAINTENANCE' (karbantartás)
|
||||
- REPAIR, ACCIDENT -> 'REPAIR' (javítás)
|
||||
- UPGRADE, RECALL -> 'UPGRADE' (fejlesztés)
|
||||
|
||||
Falls back to a generic 'MAINTENANCE' category if none found.
|
||||
"""
|
||||
category_map = {
|
||||
"SERVICE": "MAINTENANCE",
|
||||
"MAINTENANCE": "MAINTENANCE",
|
||||
"INSPECTION": "MAINTENANCE",
|
||||
"TIRE_CHANGE": "MAINTENANCE",
|
||||
"REPAIR": "REPAIR",
|
||||
"ACCIDENT": "REPAIR",
|
||||
"UPGRADE": "UPGRADE",
|
||||
"RECALL": "UPGRADE",
|
||||
}
|
||||
category_code = category_map.get(event_type, "MAINTENANCE")
|
||||
|
||||
stmt = select(CostCategory.id).where(
|
||||
CostCategory.code == category_code
|
||||
).limit(1)
|
||||
result = await db.execute(stmt)
|
||||
cat_id = result.scalar_one_or_none()
|
||||
|
||||
if cat_id is None:
|
||||
# Fallback: try to find any category with 'MAINTENANCE' in name
|
||||
fallback_stmt = select(CostCategory.id).where(
|
||||
CostCategory.code.ilike("%MAINTENANCE%")
|
||||
).limit(1)
|
||||
fallback_result = await db.execute(fallback_stmt)
|
||||
cat_id = fallback_result.scalar_one_or_none()
|
||||
|
||||
if cat_id is None:
|
||||
# Ultimate fallback: pick the first available cost category
|
||||
any_stmt = select(CostCategory.id).limit(1)
|
||||
any_result = await db.execute(any_stmt)
|
||||
cat_id = any_result.scalar_one_or_none()
|
||||
|
||||
if cat_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="No cost category found in the system. Please seed cost categories first."
|
||||
)
|
||||
|
||||
return cat_id
|
||||
|
||||
|
||||
@router.get("/{asset_id}/events", response_model=List[AssetEventResponse])
|
||||
async def list_asset_events(
|
||||
asset_id: uuid.UUID,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Szervizkönyv események listázása.
|
||||
|
||||
GET /api/v1/assets/{asset_id}/events
|
||||
|
||||
Visszaadja a járműhöz tartozó összes AssetEvent rekordot,
|
||||
dátum szerint csökkenő sorrendben (legújabb elöl).
|
||||
"""
|
||||
# Jogosultság ellenőrzés
|
||||
await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = (
|
||||
select(AssetEvent)
|
||||
.options(selectinload(AssetEvent.cost))
|
||||
.where(AssetEvent.asset_id == asset_id)
|
||||
.order_by(desc(AssetEvent.event_date))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
events = result.scalars().all()
|
||||
return events
|
||||
|
||||
|
||||
@router.post("/{asset_id}/events", response_model=AssetEventResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_asset_event(
|
||||
asset_id: uuid.UUID,
|
||||
payload: AssetEventCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Új szervizkönyv esemény rögzítése.
|
||||
|
||||
POST /api/v1/assets/{asset_id}/events
|
||||
|
||||
**Üzleti logika:**
|
||||
- Ha a beküldött `odometer_reading` nagyobb, mint a jármű jelenlegi
|
||||
`current_mileage` értéke, akkor:
|
||||
1. Létrehoz egy új OdometerReading rekordot
|
||||
2. Frissíti a jármű `current_mileage` mezőjét
|
||||
- Ha `cost_amount > 0`, automatikusan létrehoz egy AssetCost rekordot
|
||||
a megfelelő költségkategóriával (MAINTENANCE/REPAIR/SERVICE),
|
||||
és összekapcsolja az eseménnyel a `cost_id` mezőn keresztül.
|
||||
"""
|
||||
# Jogosultság ellenőrzés
|
||||
asset = await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
# Dátum: ha nincs megadva, használjuk a jelenlegi időt
|
||||
event_date = payload.event_date or datetime.now(timezone.utc)
|
||||
|
||||
# Szervezet meghatározása
|
||||
organization_id = asset.current_organization_id or asset.owner_org_id
|
||||
|
||||
try:
|
||||
# ── Üzleti logika: AssetCost automatikus létrehozása ──
|
||||
cost_id = payload.cost_id
|
||||
if payload.cost_amount is not None and payload.cost_amount > 0:
|
||||
# Költségkategória feloldása az esemény típusa alapján
|
||||
# SERVICE/MAINTENANCE -> karbantartás, REPAIR -> javítás
|
||||
cost_category_id = await _resolve_cost_category_for_event(db, payload.event_type)
|
||||
|
||||
currency = payload.currency or "HUF"
|
||||
|
||||
cost_record = AssetCost(
|
||||
asset_id=asset_id,
|
||||
organization_id=organization_id,
|
||||
category_id=cost_category_id,
|
||||
amount_net=payload.cost_amount,
|
||||
currency=currency,
|
||||
date=event_date,
|
||||
data={
|
||||
"description": payload.description or f"Service event: {payload.event_type}",
|
||||
"mileage_at_cost": payload.odometer_reading,
|
||||
"source": "service_book",
|
||||
"event_type": payload.event_type,
|
||||
},
|
||||
)
|
||||
db.add(cost_record)
|
||||
await db.flush() # Flush to get the cost_record.id
|
||||
cost_id = cost_record.id
|
||||
|
||||
logger.info(
|
||||
f"AssetCost auto-created for asset {asset_id}: "
|
||||
f"{payload.cost_amount} {currency} (event_type={payload.event_type}, cost_id={cost_id})"
|
||||
)
|
||||
|
||||
# Új esemény létrehozása
|
||||
event = AssetEvent(
|
||||
asset_id=asset_id,
|
||||
user_id=current_user.id,
|
||||
organization_id=organization_id,
|
||||
event_type=payload.event_type,
|
||||
odometer_reading=payload.odometer_reading,
|
||||
description=payload.description,
|
||||
cost_id=cost_id,
|
||||
event_date=event_date,
|
||||
)
|
||||
db.add(event)
|
||||
|
||||
# ── Üzleti logika: Km óra állás frissítése ──
|
||||
if payload.odometer_reading is not None and payload.odometer_reading > asset.current_mileage:
|
||||
# 1. Új OdometerReading rekord létrehozása
|
||||
odometer = OdometerReading(
|
||||
asset_id=asset_id,
|
||||
reading=payload.odometer_reading,
|
||||
source="manual",
|
||||
)
|
||||
db.add(odometer)
|
||||
|
||||
# 2. Jármű current_mileage frissítése
|
||||
asset.current_mileage = payload.odometer_reading
|
||||
asset.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
logger.info(
|
||||
f"Odometer updated for asset {asset_id}: "
|
||||
f"{asset.current_mileage} → {payload.odometer_reading} "
|
||||
f"(via event {payload.event_type})"
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(event, attribute_names=["cost"])
|
||||
return event
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"Asset event creation error for {asset_id}: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Hiba a szervizkönyv esemény létrehozásakor"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/vehicles/{vehicle_id}/archive", response_model=AssetResponse)
|
||||
async def archive_vehicle(
|
||||
vehicle_id: uuid.UUID,
|
||||
@@ -747,7 +987,7 @@ async def archive_vehicle(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Vehicle archive error for {vehicle_id}: {e}")
|
||||
logger.error(f"Vehicle archive error for {vehicle_id}: {e}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Belső szerverhiba a jármű archiválásakor"
|
||||
|
||||
Reference in New Issue
Block a user