frontend költség beállítások
This commit is contained in:
@@ -12,11 +12,17 @@ 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, AssetCatalog, AssetCost, AssetEvent, OdometerReading, CostCategory, OrganizationMember, OrgRole
|
||||
from app.models.fleet_finance import AssetFinancials, VehicleInsurancePolicy, VehicleTaxObligation
|
||||
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, AssetEventCreate, AssetEventResponse
|
||||
from app.schemas.fleet_finance import (
|
||||
AssetFinancialsUpdate, AssetFinancialsResponse,
|
||||
VehicleInsurancePolicyCreate, VehicleInsurancePolicyUpdate, VehicleInsurancePolicyResponse,
|
||||
VehicleTaxObligationCreate, VehicleTaxObligationUpdate, VehicleTaxObligationResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1228,4 +1234,363 @@ async def archive_vehicle(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Belső szerverhiba a jármű archiválásakor"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Fleet Finance endpoints
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get(
|
||||
"/vehicles/{asset_id}/financials",
|
||||
response_model=AssetFinancialsResponse,
|
||||
)
|
||||
async def get_vehicle_financials(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Jármű pénzügyi adatainak lekérése.
|
||||
|
||||
GET /api/v1/assets/vehicles/{asset_id}/financials
|
||||
|
||||
Visszaadja a jármű beszerzési, finanszírozási és lízing adatait.
|
||||
"""
|
||||
asset = await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = select(AssetFinancials).where(AssetFinancials.asset_id == asset_id)
|
||||
result = await db.execute(stmt)
|
||||
financials = result.scalar_one_or_none()
|
||||
|
||||
if not financials:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A járműhöz nem található pénzügyi adat"
|
||||
)
|
||||
return financials
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/vehicles/{asset_id}/financials",
|
||||
response_model=AssetFinancialsResponse,
|
||||
)
|
||||
async def update_vehicle_financials(
|
||||
asset_id: uuid.UUID,
|
||||
payload: AssetFinancialsUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Jármű pénzügyi adatainak frissítése.
|
||||
|
||||
PATCH /api/v1/assets/vehicles/{asset_id}/financials
|
||||
|
||||
Csak a megadott mezőket frissíti (partial update).
|
||||
"""
|
||||
asset = await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = select(AssetFinancials).where(AssetFinancials.asset_id == asset_id)
|
||||
result = await db.execute(stmt)
|
||||
financials = result.scalar_one_or_none()
|
||||
|
||||
if not financials:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="A járműhöz nem található pénzügyi adat"
|
||||
)
|
||||
|
||||
# Partial update: csak a megadott mezőket frissítjük
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(financials, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(financials)
|
||||
return financials
|
||||
|
||||
|
||||
@router.get(
|
||||
"/vehicles/{asset_id}/insurance",
|
||||
response_model=List[VehicleInsurancePolicyResponse],
|
||||
)
|
||||
async def list_vehicle_insurance_policies(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Jármű biztosítási kötvényeinek listázása.
|
||||
|
||||
GET /api/v1/assets/vehicles/{asset_id}/insurance
|
||||
|
||||
Visszaadja a járműhöz tartozó összes biztosítási kötvényt.
|
||||
"""
|
||||
asset = await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = (
|
||||
select(VehicleInsurancePolicy)
|
||||
.where(VehicleInsurancePolicy.asset_id == asset_id)
|
||||
.order_by(VehicleInsurancePolicy.start_date.desc())
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
policies = result.scalars().all()
|
||||
return policies
|
||||
|
||||
|
||||
@router.post(
|
||||
"/vehicles/{asset_id}/insurance",
|
||||
response_model=VehicleInsurancePolicyResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_vehicle_insurance_policy(
|
||||
asset_id: uuid.UUID,
|
||||
payload: VehicleInsurancePolicyCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Új biztosítási kötvény létrehozása a járműhöz.
|
||||
|
||||
POST /api/v1/assets/vehicles/{asset_id}/insurance
|
||||
"""
|
||||
asset = await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
policy = VehicleInsurancePolicy(
|
||||
asset_id=asset_id,
|
||||
provider_id=payload.provider_id,
|
||||
insurance_type=payload.insurance_type,
|
||||
policy_number=payload.policy_number,
|
||||
start_date=payload.start_date,
|
||||
expiry_date=payload.expiry_date,
|
||||
premium_amount=payload.premium_amount,
|
||||
currency=payload.currency,
|
||||
)
|
||||
db.add(policy)
|
||||
await db.commit()
|
||||
await db.refresh(policy)
|
||||
return policy
|
||||
|
||||
|
||||
@router.get(
|
||||
"/vehicles/{asset_id}/tax",
|
||||
response_model=List[VehicleTaxObligationResponse],
|
||||
)
|
||||
async def list_vehicle_tax_obligations(
|
||||
asset_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Jármű adókötelezettségeinek listázása.
|
||||
|
||||
GET /api/v1/assets/vehicles/{asset_id}/tax
|
||||
|
||||
Visszaadja a járműhöz tartozó összes adókötelezettséget.
|
||||
"""
|
||||
asset = await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = (
|
||||
select(VehicleTaxObligation)
|
||||
.where(VehicleTaxObligation.asset_id == asset_id)
|
||||
.order_by(VehicleTaxObligation.tax_year.desc())
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
obligations = result.scalars().all()
|
||||
return obligations
|
||||
|
||||
|
||||
@router.post(
|
||||
"/vehicles/{asset_id}/tax",
|
||||
response_model=VehicleTaxObligationResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_vehicle_tax_obligation(
|
||||
asset_id: uuid.UUID,
|
||||
payload: VehicleTaxObligationCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Új adókötelezettség létrehozása a járműhöz.
|
||||
|
||||
POST /api/v1/assets/vehicles/{asset_id}/tax
|
||||
"""
|
||||
asset = await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
obligation = VehicleTaxObligation(
|
||||
asset_id=asset_id,
|
||||
tax_type=payload.tax_type,
|
||||
tax_year=payload.tax_year,
|
||||
amount=payload.amount,
|
||||
currency=payload.currency,
|
||||
due_date=payload.due_date,
|
||||
payment_status=payload.payment_status,
|
||||
)
|
||||
db.add(obligation)
|
||||
await db.commit()
|
||||
await db.refresh(obligation)
|
||||
return obligation
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VehicleInsurancePolicy PUT / DELETE
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.put(
|
||||
"/vehicles/{asset_id}/insurance/{policy_id}",
|
||||
response_model=VehicleInsurancePolicyResponse,
|
||||
)
|
||||
async def update_vehicle_insurance_policy(
|
||||
asset_id: uuid.UUID,
|
||||
policy_id: uuid.UUID,
|
||||
payload: VehicleInsurancePolicyUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Biztosítási kötvény módosítása.
|
||||
|
||||
PUT /api/v1/assets/vehicles/{asset_id}/insurance/{policy_id}
|
||||
|
||||
Csak a megadott mezőket frissíti (partial update).
|
||||
"""
|
||||
await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = select(VehicleInsurancePolicy).where(
|
||||
VehicleInsurancePolicy.id == policy_id,
|
||||
VehicleInsurancePolicy.asset_id == asset_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
policy = result.scalar_one_or_none()
|
||||
|
||||
if not policy:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Biztosítási kötvény nem található",
|
||||
)
|
||||
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(policy, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(policy)
|
||||
return policy
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/vehicles/{asset_id}/insurance/{policy_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_vehicle_insurance_policy(
|
||||
asset_id: uuid.UUID,
|
||||
policy_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Biztosítási kötvény törlése.
|
||||
|
||||
DELETE /api/v1/assets/vehicles/{asset_id}/insurance/{policy_id}
|
||||
"""
|
||||
await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = select(VehicleInsurancePolicy).where(
|
||||
VehicleInsurancePolicy.id == policy_id,
|
||||
VehicleInsurancePolicy.asset_id == asset_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
policy = result.scalar_one_or_none()
|
||||
|
||||
if not policy:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Biztosítási kötvény nem található",
|
||||
)
|
||||
|
||||
await db.delete(policy)
|
||||
await db.commit()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VehicleTaxObligation PUT / DELETE
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.put(
|
||||
"/vehicles/{asset_id}/tax/{tax_id}",
|
||||
response_model=VehicleTaxObligationResponse,
|
||||
)
|
||||
async def update_vehicle_tax_obligation(
|
||||
asset_id: uuid.UUID,
|
||||
tax_id: uuid.UUID,
|
||||
payload: VehicleTaxObligationUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Adókötelezettség módosítása.
|
||||
|
||||
PUT /api/v1/assets/vehicles/{asset_id}/tax/{tax_id}
|
||||
|
||||
Csak a megadott mezőket frissíti (partial update).
|
||||
"""
|
||||
await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = select(VehicleTaxObligation).where(
|
||||
VehicleTaxObligation.id == tax_id,
|
||||
VehicleTaxObligation.asset_id == asset_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
obligation = result.scalar_one_or_none()
|
||||
|
||||
if not obligation:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Adókötelezettség nem található",
|
||||
)
|
||||
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(obligation, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(obligation)
|
||||
return obligation
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/vehicles/{asset_id}/tax/{tax_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_vehicle_tax_obligation(
|
||||
asset_id: uuid.UUID,
|
||||
tax_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Adókötelezettség törlése.
|
||||
|
||||
DELETE /api/v1/assets/vehicles/{asset_id}/tax/{tax_id}
|
||||
"""
|
||||
await _check_asset_access(db, asset_id, current_user)
|
||||
|
||||
stmt = select(VehicleTaxObligation).where(
|
||||
VehicleTaxObligation.id == tax_id,
|
||||
VehicleTaxObligation.asset_id == asset_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
obligation = result.scalar_one_or_none()
|
||||
|
||||
if not obligation:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Adókötelezettség nem található",
|
||||
)
|
||||
|
||||
await db.delete(obligation)
|
||||
await db.commit()
|
||||
@@ -2,15 +2,21 @@
|
||||
"""
|
||||
Szótárak és katalógusok végpontjai.
|
||||
- GET /dictionaries/cost-categories: Költségkategóriák lekérése visibility szerint szűrve
|
||||
- CRUD /dictionaries/insurance-providers: Biztosítók katalógusa
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import select, desc
|
||||
from app.db.session import get_db
|
||||
from app.api import deps
|
||||
from app.models.vehicle import CostCategory
|
||||
from app.models.fleet_finance import CostCategory, InsuranceProvider
|
||||
from app.schemas.fleet_finance import (
|
||||
InsuranceProviderResponse,
|
||||
InsuranceProviderCreate,
|
||||
InsuranceProviderUpdate,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -66,3 +72,172 @@ async def list_cost_categories(
|
||||
root_categories.append(cat_dict)
|
||||
|
||||
return root_categories
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# InsuranceProvider CRUD
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@router.get(
|
||||
"/insurance-providers",
|
||||
response_model=List[InsuranceProviderResponse],
|
||||
)
|
||||
async def list_insurance_providers(
|
||||
active_only: bool = Query(True, description="Csak az aktív biztosítók listázása"),
|
||||
country_code: Optional[str] = Query(None, max_length=2, description="ISO 3166-1 alpha-2 országkód szerinti szűrés (pl. 'HU'). Visszaadja az adott ország és a globális (NULL) szolgáltatókat."),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(deps.get_current_user),
|
||||
):
|
||||
"""
|
||||
Biztosítók listázása.
|
||||
Alapértelmezés szerint csak az aktív biztosítókat adja vissza (frontend dropdown számára).
|
||||
Opcionálisan szűrhető country_code alapján: ha meg van adva, visszaadja az adott ország
|
||||
szolgáltatóit ÉS a globális (country_code IS NULL) szolgáltatókat is.
|
||||
"""
|
||||
stmt = select(InsuranceProvider).order_by(InsuranceProvider.name)
|
||||
|
||||
if active_only:
|
||||
stmt = stmt.where(InsuranceProvider.is_active == True)
|
||||
|
||||
if country_code:
|
||||
stmt = stmt.where(
|
||||
(InsuranceProvider.country_code == country_code) |
|
||||
(InsuranceProvider.country_code.is_(None))
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
providers = result.scalars().all()
|
||||
return providers
|
||||
|
||||
|
||||
@router.get(
|
||||
"/insurance-providers/{provider_id}",
|
||||
response_model=InsuranceProviderResponse,
|
||||
)
|
||||
async def get_insurance_provider(
|
||||
provider_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(deps.get_current_user),
|
||||
):
|
||||
"""
|
||||
Egy biztosító adatainak lekérése ID alapján.
|
||||
"""
|
||||
stmt = select(InsuranceProvider).where(InsuranceProvider.id == provider_id)
|
||||
result = await db.execute(stmt)
|
||||
provider = result.scalar_one_or_none()
|
||||
|
||||
if not provider:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Biztosító nem található",
|
||||
)
|
||||
return provider
|
||||
|
||||
|
||||
@router.post(
|
||||
"/insurance-providers",
|
||||
response_model=InsuranceProviderResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def create_insurance_provider(
|
||||
payload: InsuranceProviderCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(deps.get_current_user),
|
||||
):
|
||||
"""
|
||||
Új biztosító hozzáadása a katalógushoz.
|
||||
"""
|
||||
# Check for duplicate name
|
||||
existing_stmt = select(InsuranceProvider).where(
|
||||
InsuranceProvider.name == payload.name
|
||||
)
|
||||
existing_result = await db.execute(existing_stmt)
|
||||
if existing_result.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Már létezik biztosító ezzel a névvel: '{payload.name}'",
|
||||
)
|
||||
|
||||
provider = InsuranceProvider(
|
||||
name=payload.name,
|
||||
claim_phone=payload.claim_phone,
|
||||
claim_url=payload.claim_url,
|
||||
services_offered=payload.services_offered or {},
|
||||
is_active=payload.is_active,
|
||||
)
|
||||
db.add(provider)
|
||||
await db.commit()
|
||||
await db.refresh(provider)
|
||||
return provider
|
||||
|
||||
|
||||
@router.put(
|
||||
"/insurance-providers/{provider_id}",
|
||||
response_model=InsuranceProviderResponse,
|
||||
)
|
||||
async def update_insurance_provider(
|
||||
provider_id: int,
|
||||
payload: InsuranceProviderUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(deps.get_current_user),
|
||||
):
|
||||
"""
|
||||
Biztosító adatainak módosítása.
|
||||
"""
|
||||
stmt = select(InsuranceProvider).where(InsuranceProvider.id == provider_id)
|
||||
result = await db.execute(stmt)
|
||||
provider = result.scalar_one_or_none()
|
||||
|
||||
if not provider:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Biztosító nem található",
|
||||
)
|
||||
|
||||
# Check for duplicate name if name is being changed
|
||||
if payload.name is not None and payload.name != provider.name:
|
||||
dup_stmt = select(InsuranceProvider).where(
|
||||
InsuranceProvider.name == payload.name,
|
||||
InsuranceProvider.id != provider_id,
|
||||
)
|
||||
dup_result = await db.execute(dup_stmt)
|
||||
if dup_result.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Már létezik biztosító ezzel a névvel: '{payload.name}'",
|
||||
)
|
||||
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(provider, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(provider)
|
||||
return provider
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/insurance-providers/{provider_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
async def delete_insurance_provider(
|
||||
provider_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(deps.get_current_user),
|
||||
):
|
||||
"""
|
||||
Biztosító törlése a katalógusból.
|
||||
"""
|
||||
stmt = select(InsuranceProvider).where(InsuranceProvider.id == provider_id)
|
||||
result = await db.execute(stmt)
|
||||
provider = result.scalar_one_or_none()
|
||||
|
||||
if not provider:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Biztosító nem található",
|
||||
)
|
||||
|
||||
await db.delete(provider)
|
||||
await db.commit()
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/expenses.py
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy import select, func, desc
|
||||
from app.api.deps import get_db, get_current_user, RequireOrgCapability
|
||||
from app.models import Asset, AssetCost, AssetEvent, OrganizationMember, SystemParameter, OrgRole
|
||||
from app.models import Asset, AssetCost, AssetEvent, OrganizationMember, SystemParameter, OrgRole, CostCategory, Organization
|
||||
from app.schemas.asset_cost import AssetCostCreate
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -110,6 +112,236 @@ def _calculate_net_from_gross(amount_gross: Decimal, vat_rate: Decimal) -> Decim
|
||||
return (amount_gross / (Decimal("1") + vat_rate / Decimal("100"))).quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_all_expenses(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||
asset_id: Optional[str] = Query(None, description="Filter by asset UUID"),
|
||||
organization_id: Optional[int] = Query(None, description="Filter by organization ID. If provided, user must be a member."),
|
||||
):
|
||||
"""
|
||||
List all expenses across all assets for the current user's organization,
|
||||
ordered by date descending with pagination.
|
||||
Supports optional asset_id filter for vehicle-specific cost views.
|
||||
Supports optional organization_id filter for cross-org views (user must be a member).
|
||||
Returns enriched expense data including vehicle info, category name, and vendor.
|
||||
"""
|
||||
# Resolve organization: use explicit organization_id if provided, otherwise fallback to user's active org
|
||||
if organization_id is not None:
|
||||
# Verify user is a member of the requested organization
|
||||
org_stmt = select(OrganizationMember).where(
|
||||
OrganizationMember.user_id == current_user.id,
|
||||
OrganizationMember.organization_id == organization_id,
|
||||
OrganizationMember.status == "active"
|
||||
).limit(1)
|
||||
org_result = await db.execute(org_stmt)
|
||||
membership = org_result.scalar_one_or_none()
|
||||
if not membership:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You are not an active member of the requested organization."
|
||||
)
|
||||
org_id = organization_id
|
||||
else:
|
||||
# Fallback: resolve user's first active organization
|
||||
org_stmt = select(OrganizationMember).where(
|
||||
OrganizationMember.user_id == current_user.id,
|
||||
OrganizationMember.status == "active"
|
||||
).limit(1)
|
||||
org_result = await db.execute(org_stmt)
|
||||
membership = org_result.scalar_one_or_none()
|
||||
if not membership:
|
||||
raise HTTPException(status_code=403, detail="No active organization membership found.")
|
||||
org_id = membership.organization_id
|
||||
|
||||
# Calculate offset
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# Build base query with joins
|
||||
base_select = (
|
||||
select(
|
||||
AssetCost,
|
||||
CostCategory.code,
|
||||
CostCategory.name,
|
||||
Organization.name,
|
||||
Asset.license_plate,
|
||||
Asset.brand,
|
||||
Asset.model,
|
||||
)
|
||||
.outerjoin(CostCategory, AssetCost.category_id == CostCategory.id)
|
||||
.outerjoin(Organization, AssetCost.vendor_organization_id == Organization.id)
|
||||
.join(Asset, AssetCost.asset_id == Asset.id)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
filters = [AssetCost.organization_id == org_id]
|
||||
if asset_id:
|
||||
filters.append(AssetCost.asset_id == asset_id)
|
||||
|
||||
expense_stmt = (
|
||||
base_select
|
||||
.where(*filters)
|
||||
.order_by(desc(AssetCost.date))
|
||||
.offset(offset)
|
||||
.limit(page_size)
|
||||
)
|
||||
expense_result = await db.execute(expense_stmt)
|
||||
rows = expense_result.all()
|
||||
|
||||
# Build enriched response
|
||||
expenses = []
|
||||
for row in rows:
|
||||
cost = row[0]
|
||||
cat_code = row[1]
|
||||
cat_name = row[2]
|
||||
vendor_name = row[3]
|
||||
license_plate = row[4]
|
||||
brand = row[5]
|
||||
model = row[6]
|
||||
|
||||
data = cost.data or {}
|
||||
description = data.get("description")
|
||||
mileage_at_cost = data.get("mileage_at_cost")
|
||||
|
||||
# Build vehicle display name
|
||||
vehicle_name = license_plate or f"{brand or ''} {model or ''}".strip() or "Unknown"
|
||||
|
||||
expenses.append({
|
||||
"id": str(cost.id),
|
||||
"asset_id": str(cost.asset_id),
|
||||
"organization_id": cost.organization_id,
|
||||
"category_id": cost.category_id,
|
||||
"category_code": cat_code,
|
||||
"category_name": cat_name,
|
||||
"amount_gross": str(cost.amount_gross) if cost.amount_gross else None,
|
||||
"amount_net": str(cost.amount_net),
|
||||
"vat_rate": str(cost.vat_rate) if cost.vat_rate else None,
|
||||
"currency": cost.currency,
|
||||
"date": cost.date.isoformat() if cost.date else None,
|
||||
"status": cost.status,
|
||||
"description": description,
|
||||
"mileage_at_cost": mileage_at_cost,
|
||||
"invoice_number": cost.invoice_number,
|
||||
"linked_asset_event_id": str(cost.linked_asset_event_id) if cost.linked_asset_event_id else None,
|
||||
"vendor_organization_id": cost.vendor_organization_id,
|
||||
"external_vendor_name": cost.external_vendor_name,
|
||||
"vendor_name": vendor_name,
|
||||
"invoice_date": cost.invoice_date.isoformat() if cost.invoice_date else None,
|
||||
"fulfillment_date": cost.fulfillment_date.isoformat() if cost.fulfillment_date else None,
|
||||
# Vehicle info
|
||||
"vehicle_name": vehicle_name,
|
||||
"license_plate": license_plate,
|
||||
})
|
||||
|
||||
# Count total for pagination (respect asset_id filter)
|
||||
count_filters = [AssetCost.organization_id == org_id]
|
||||
if asset_id:
|
||||
count_filters.append(AssetCost.asset_id == asset_id)
|
||||
count_stmt = (
|
||||
select(func.count(AssetCost.id))
|
||||
.where(*count_filters)
|
||||
)
|
||||
count_result = await db.execute(count_stmt)
|
||||
total = count_result.scalar()
|
||||
|
||||
total_pages = max(1, (total + page_size - 1) // page_size)
|
||||
|
||||
return {
|
||||
"data": expenses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": total_pages,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{asset_id}")
|
||||
async def list_asset_expenses(
|
||||
asset_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user = Depends(get_current_user),
|
||||
limit: int = Query(50, ge=1, le=200, description="Maximum number of expenses to return"),
|
||||
offset: int = Query(0, ge=0, description="Number of expenses to skip"),
|
||||
):
|
||||
"""
|
||||
List all expenses for a specific asset, ordered by date descending.
|
||||
Returns enriched expense data including category name, code, and vendor info.
|
||||
Used by the OverviewTab and CostManagerModal to display real expense data.
|
||||
"""
|
||||
# Validate asset exists
|
||||
stmt = select(Asset).where(Asset.id == asset_id)
|
||||
result = await db.execute(stmt)
|
||||
asset = result.scalar_one_or_none()
|
||||
if not asset:
|
||||
raise HTTPException(status_code=404, detail="Asset not found.")
|
||||
|
||||
# Fetch expenses with category join and vendor organization join
|
||||
expense_stmt = (
|
||||
select(AssetCost, CostCategory.code, CostCategory.name, Organization.name)
|
||||
.outerjoin(CostCategory, AssetCost.category_id == CostCategory.id)
|
||||
.outerjoin(Organization, AssetCost.vendor_organization_id == Organization.id)
|
||||
.where(AssetCost.asset_id == asset_id)
|
||||
.order_by(desc(AssetCost.date))
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
expense_result = await db.execute(expense_stmt)
|
||||
rows = expense_result.all()
|
||||
|
||||
# Build enriched response
|
||||
expenses = []
|
||||
for row in rows:
|
||||
cost = row[0] # AssetCost instance
|
||||
cat_code = row[1] # CostCategory.code
|
||||
cat_name = row[2] # CostCategory.name
|
||||
vendor_name = row[3] # Organization.name (resolved from vendor_organization_id)
|
||||
|
||||
# Extract description and mileage from data JSONB
|
||||
data = cost.data or {}
|
||||
description = data.get("description")
|
||||
mileage_at_cost = data.get("mileage_at_cost")
|
||||
|
||||
expenses.append({
|
||||
"id": str(cost.id),
|
||||
"asset_id": str(cost.asset_id),
|
||||
"organization_id": cost.organization_id,
|
||||
"category_id": cost.category_id,
|
||||
"category_code": cat_code,
|
||||
"category_name": cat_name,
|
||||
"amount_gross": str(cost.amount_gross) if cost.amount_gross else None,
|
||||
"amount_net": str(cost.amount_net),
|
||||
"vat_rate": str(cost.vat_rate) if cost.vat_rate else None,
|
||||
"currency": cost.currency,
|
||||
"date": cost.date.isoformat() if cost.date else None,
|
||||
"status": cost.status,
|
||||
"description": description,
|
||||
"mileage_at_cost": mileage_at_cost,
|
||||
"invoice_number": cost.invoice_number,
|
||||
"linked_asset_event_id": str(cost.linked_asset_event_id) if cost.linked_asset_event_id else None,
|
||||
# === B2B VENDOR FIELDS ===
|
||||
"vendor_organization_id": cost.vendor_organization_id,
|
||||
"external_vendor_name": cost.external_vendor_name,
|
||||
"vendor_name": vendor_name, # Enriched from fleet.organizations.name
|
||||
# === INVOICE DATES ===
|
||||
"invoice_date": cost.invoice_date.isoformat() if cost.invoice_date else None,
|
||||
"fulfillment_date": cost.fulfillment_date.isoformat() if cost.fulfillment_date else None,
|
||||
})
|
||||
|
||||
# Count total for pagination
|
||||
count_stmt = select(func.count(AssetCost.id)).where(AssetCost.asset_id == asset_id)
|
||||
count_result = await db.execute(count_stmt)
|
||||
total = count_result.scalar()
|
||||
|
||||
return {
|
||||
"data": expenses,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
async def create_expense(
|
||||
expense: AssetCostCreate,
|
||||
@@ -247,10 +479,16 @@ async def create_expense(
|
||||
amount_gross=amount_gross,
|
||||
vat_rate=vat_rate,
|
||||
currency=expense.currency,
|
||||
date=expense.date,
|
||||
date=expense.cost_date,
|
||||
invoice_number=data.get("invoice_number"),
|
||||
status=expense_status,
|
||||
data=data
|
||||
data=data,
|
||||
# === B2B VENDOR FIELDS ===
|
||||
vendor_organization_id=expense.vendor_organization_id,
|
||||
external_vendor_name=expense.external_vendor_name,
|
||||
# === INVOICE DATES ===
|
||||
invoice_date=expense.invoice_date,
|
||||
fulfillment_date=expense.fulfillment_date,
|
||||
)
|
||||
|
||||
db.add(new_cost)
|
||||
@@ -279,7 +517,7 @@ async def create_expense(
|
||||
cost_id=new_cost.id,
|
||||
linked_expense_id=new_cost.id, # Bidirectional link
|
||||
status=event_status,
|
||||
event_date=expense.date or datetime.now(timezone.utc),
|
||||
event_date=expense.cost_date or datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(new_event)
|
||||
await db.flush() # Flush to get new_event.id
|
||||
|
||||
@@ -184,7 +184,11 @@ async def get_my_organizations(
|
||||
):
|
||||
"""
|
||||
A bejelentkezett felhasználóhoz tartozó szervezetek listázása.
|
||||
P0: Minden szervezethez visszaadja a valós max_vehicles és max_garages limiteket
|
||||
a subscription_tier JSONB rules alapján.
|
||||
"""
|
||||
from app.models.core_logic import SubscriptionTier, OrganizationSubscription
|
||||
|
||||
subq = (
|
||||
select(Organization.id)
|
||||
.outerjoin(OrganizationMember, OrganizationMember.organization_id == Organization.id)
|
||||
@@ -206,6 +210,31 @@ async def get_my_organizations(
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
orgs = result.scalars().all()
|
||||
|
||||
# ── P0: Pre-fetch all org subscription tiers for limit resolution ──
|
||||
org_ids = [o.id for o in orgs]
|
||||
org_tier_map: dict[int, tuple[int, int]] = {}
|
||||
if org_ids:
|
||||
org_subs_stmt = (
|
||||
select(OrganizationSubscription.org_id, SubscriptionTier)
|
||||
.join(SubscriptionTier, OrganizationSubscription.tier_id == SubscriptionTier.id)
|
||||
.where(
|
||||
OrganizationSubscription.org_id.in_(org_ids),
|
||||
OrganizationSubscription.is_active == True
|
||||
)
|
||||
.distinct(OrganizationSubscription.org_id)
|
||||
.order_by(OrganizationSubscription.org_id, OrganizationSubscription.valid_from.desc())
|
||||
)
|
||||
org_subs_result = await db.execute(org_subs_stmt)
|
||||
for row in org_subs_result:
|
||||
o_id = row.org_id
|
||||
tier = row[1]
|
||||
if tier and tier.rules:
|
||||
allowances = tier.rules.get("allowances", {})
|
||||
org_tier_map[o_id] = (
|
||||
int(allowances.get("max_vehicles", 1)),
|
||||
int(allowances.get("max_garages", 1)),
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -220,6 +249,11 @@ async def get_my_organizations(
|
||||
"is_active": o.is_active,
|
||||
"is_deleted": o.is_deleted,
|
||||
"subscription_plan": o.subscription_plan,
|
||||
"subscription_expires_at": o.subscription_expires_at.isoformat() if o.subscription_expires_at else None,
|
||||
"subscription_tier_id": o.subscription_tier_id,
|
||||
# P0: Real limits from subscription tier
|
||||
"max_vehicles": org_tier_map.get(o.id, (o.base_asset_limit or 1, 1))[0],
|
||||
"max_garages": org_tier_map.get(o.id, (1, 1))[1],
|
||||
"org_type": o.org_type.value if hasattr(o.org_type, 'value') else str(o.org_type),
|
||||
"visual_settings": o.visual_settings if hasattr(o, 'visual_settings') else None,
|
||||
"user_role": _get_user_role(o, current_user.id),
|
||||
|
||||
@@ -1,56 +1,97 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
# /opt/docker/dev/service_finder/backend/app/api/v1/endpoints/reports.py
|
||||
"""
|
||||
Reports endpoints — Zombie API Cleanup.
|
||||
Replaced raw SQL (FROM vehicle.vehicle_expenses) with SQLAlchemy ORM queries
|
||||
using fleet_finance.asset_costs (AssetCost) + CostCategory.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import select, func, desc
|
||||
from sqlalchemy.orm import selectinload
|
||||
from app.api.deps import get_db, get_current_user
|
||||
from app.models.fleet_finance import AssetCost, CostCategory
|
||||
from app.models.identity import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router = APIRouter() # EZ HIÁNYZOTT!
|
||||
|
||||
@router.get("/summary/{vehicle_id}")
|
||||
async def get_vehicle_summary(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
async def get_vehicle_summary(
|
||||
vehicle_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Összesített jelentés egy járműhöz: kategóriánkénti költségek.
|
||||
Uses fleet_finance.asset_costs (AssetCost) joined with CostCategory.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
category,
|
||||
SUM(amount) as total_amount,
|
||||
COUNT(*) as transaction_count
|
||||
FROM vehicle.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY category
|
||||
""")
|
||||
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
rows = result.fetchall()
|
||||
|
||||
total_cost = sum(row.total_amount for row in rows) if rows else 0
|
||||
|
||||
# Aggregate costs by category using ORM
|
||||
stmt = (
|
||||
select(
|
||||
CostCategory.name.label("category"),
|
||||
func.sum(AssetCost.amount_gross).label("total_amount"),
|
||||
func.count(AssetCost.id).label("transaction_count"),
|
||||
)
|
||||
.join(CostCategory, AssetCost.category_id == CostCategory.id)
|
||||
.where(AssetCost.asset_id == vehicle_id)
|
||||
.group_by(CostCategory.name)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
total_cost = sum(float(row.total_amount) for row in rows) if rows else 0.0
|
||||
|
||||
return {
|
||||
"vehicle_id": vehicle_id,
|
||||
"total_cost": float(total_cost),
|
||||
"breakdown": [dict(row._mapping) for row in rows]
|
||||
"total_cost": total_cost,
|
||||
"breakdown": [
|
||||
{
|
||||
"category": row.category,
|
||||
"total_amount": float(row.total_amount),
|
||||
"transaction_count": row.transaction_count,
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/trends/{vehicle_id}")
|
||||
async def get_monthly_trends(vehicle_id: str, db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
async def get_monthly_trends(
|
||||
vehicle_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Visszaadja az utolsó 6 hónap költéseit havi bontásban.
|
||||
Uses fleet_finance.asset_costs (AssetCost) with date truncation.
|
||||
"""
|
||||
query = text("""
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
SUM(amount) as monthly_total
|
||||
FROM vehicle.vehicle_expenses
|
||||
WHERE vehicle_id = :v_id
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
LIMIT 6
|
||||
""")
|
||||
result = await db.execute(query, {"v_id": vehicle_id})
|
||||
return [dict(row._mapping) for row in result.fetchall()]
|
||||
# Monthly aggregation using ORM-compatible date truncation
|
||||
from sqlalchemy import text as sql_text
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
func.to_char(AssetCost.date, "YYYY-MM").label("month"),
|
||||
func.sum(AssetCost.amount_gross).label("monthly_total"),
|
||||
)
|
||||
.where(AssetCost.asset_id == vehicle_id)
|
||||
.group_by(sql_text("month"))
|
||||
.order_by(desc(sql_text("month")))
|
||||
.limit(6)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
rows = result.all()
|
||||
|
||||
return [
|
||||
{"month": row.month, "monthly_total": float(row.monthly_total)}
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/summary/latest")
|
||||
async def get_latest_summary(db: AsyncSession = Depends(get_db), current_user = Depends(get_current_user)):
|
||||
async def get_latest_summary(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Returns a simple summary for the dashboard (mock data for now).
|
||||
This endpoint is called by the frontend dashboard.
|
||||
@@ -61,5 +102,5 @@ async def get_latest_summary(db: AsyncSession = Depends(get_db), current_user =
|
||||
"total_cost_this_month": 1250.50,
|
||||
"most_expensive_category": "Fuel",
|
||||
"trend": "down",
|
||||
"trend_percentage": -5.2
|
||||
}
|
||||
"trend_percentage": -5.2,
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ def _build_user_response(user: User, active_org_id: Optional[int] = None, db: Op
|
||||
Segédfüggvény a UserResponse dict előállításához.
|
||||
Beágyazza a Person és Address adatokat a 'person' mezőbe.
|
||||
RBAC Phase 3: system_capabilities és org_capabilities is bekerül a válaszba.
|
||||
P0: max_vehicles és max_garages a subscription_tier JSONB rules-ból.
|
||||
"""
|
||||
from app.models.marketplace.organization import Organization, OrganizationMember
|
||||
from app.models.marketplace.organization import OrgUserRole
|
||||
@@ -60,8 +61,6 @@ def _build_user_response(user: User, active_org_id: Optional[int] = None, db: Op
|
||||
active_org_id = None
|
||||
|
||||
if active_org_id is None:
|
||||
# 1. Check if user is a member of any organization with ADMIN/OWNER role
|
||||
# (This is a helper - in real usage the caller should pass active_org_id)
|
||||
pass
|
||||
|
||||
person = user.person
|
||||
@@ -110,10 +109,13 @@ def _build_user_response(user: User, active_org_id: Optional[int] = None, db: Op
|
||||
# ── RBAC Phase 3: Resolve org capabilities ──
|
||||
org_capabilities: Dict[str, Dict[str, bool]] = {}
|
||||
if db is not None:
|
||||
# We need to run this synchronously since _build_user_response is not async
|
||||
# The async version is handled in read_users_me directly
|
||||
pass
|
||||
|
||||
# ── P0: Resolve subscription limits ──
|
||||
# Default fallback values
|
||||
max_vehicles = 1
|
||||
max_garages = 1
|
||||
|
||||
return {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
@@ -124,6 +126,8 @@ def _build_user_response(user: User, active_org_id: Optional[int] = None, db: Op
|
||||
"person_id": user.person_id,
|
||||
"role": role_key,
|
||||
"subscription_plan": user.subscription_plan,
|
||||
"max_vehicles": max_vehicles,
|
||||
"max_garages": max_garages,
|
||||
"scope_level": user.scope_level or "individual",
|
||||
"scope_id": str(active_org_id) if active_org_id else None,
|
||||
"ui_mode": user.ui_mode or "personal",
|
||||
@@ -198,9 +202,60 @@ async def read_users_me(
|
||||
# Check if user is the last admin in any organization
|
||||
is_last_admin = await AuthService.check_is_last_admin(db, current_user.id)
|
||||
|
||||
# ── P0: Resolve real subscription limits from the assigned tier ──
|
||||
from app.models.core_logic import SubscriptionTier, OrganizationSubscription, UserSubscription
|
||||
max_vehicles = 1
|
||||
max_garages = 1
|
||||
|
||||
if active_org_id is not None:
|
||||
# Try org-level subscription first
|
||||
org_sub_stmt = (
|
||||
select(SubscriptionTier)
|
||||
.select_from(OrganizationSubscription)
|
||||
.join(SubscriptionTier, OrganizationSubscription.tier_id == SubscriptionTier.id)
|
||||
.where(
|
||||
OrganizationSubscription.org_id == active_org_id,
|
||||
OrganizationSubscription.is_active == True
|
||||
)
|
||||
.order_by(OrganizationSubscription.valid_from.desc())
|
||||
.limit(1)
|
||||
)
|
||||
tier = (await db.execute(org_sub_stmt)).scalar_one_or_none()
|
||||
if tier and tier.rules:
|
||||
allowances = tier.rules.get("allowances", {})
|
||||
max_vehicles = int(allowances.get("max_vehicles", 1))
|
||||
max_garages = int(allowances.get("max_garages", 1))
|
||||
else:
|
||||
# Fallback: read base_asset_limit from Organization record
|
||||
org_stmt = select(Organization).where(Organization.id == active_org_id)
|
||||
org = (await db.execute(org_stmt)).scalar_one_or_none()
|
||||
if org:
|
||||
max_vehicles = org.base_asset_limit or 1
|
||||
max_garages = 1 # No base_garage_limit column, default to 1
|
||||
else:
|
||||
# Personal mode: try user-level subscription
|
||||
user_sub_stmt = (
|
||||
select(SubscriptionTier)
|
||||
.select_from(UserSubscription)
|
||||
.join(SubscriptionTier, UserSubscription.tier_id == SubscriptionTier.id)
|
||||
.where(
|
||||
UserSubscription.user_id == current_user.id,
|
||||
UserSubscription.is_active == True
|
||||
)
|
||||
.order_by(UserSubscription.valid_from.desc())
|
||||
.limit(1)
|
||||
)
|
||||
tier = (await db.execute(user_sub_stmt)).scalar_one_or_none()
|
||||
if tier and tier.rules:
|
||||
allowances = tier.rules.get("allowances", {})
|
||||
max_vehicles = int(allowances.get("max_vehicles", 1))
|
||||
max_garages = int(allowances.get("max_garages", 1))
|
||||
|
||||
# ── RBAC Phase 3: Build base response ──
|
||||
response_data = _build_user_response(current_user, active_org_id)
|
||||
response_data["is_last_admin"] = is_last_admin
|
||||
response_data["max_vehicles"] = max_vehicles
|
||||
response_data["max_garages"] = max_garages
|
||||
|
||||
# ── RBAC Phase 3: Resolve org_capabilities ──
|
||||
# Get all organizations the user is a member of
|
||||
|
||||
@@ -214,24 +214,16 @@ MOTORCYCLE_FEATURES: dict[str, dict[str, str]] = {
|
||||
"catalyst": "Katalizátor",
|
||||
"electric_starter": "Önindító",
|
||||
"all_wheel_drive": "Összkerékhajtás",
|
||||
"alarm": "Riasztó",
|
||||
"sport_exhaust": "Sport kipufogó",
|
||||
"sport_air_filter": "Sport légszűrő",
|
||||
"cruise_control": "Tempomat",
|
||||
"turbo": "Turbó",
|
||||
"12v_system": "12 V rendszer",
|
||||
"heated_grip": "Markolat fűtés",
|
||||
"abs": "ABS (blokkolásgátló)",
|
||||
"seat_belt": "Biztonsági öv",
|
||||
"dtc": "DTC",
|
||||
"fog_light": "Ködlámpa",
|
||||
"airbag": "Légzsák",
|
||||
"xenon_headlight": "Xenon fényszóró",
|
||||
},
|
||||
"frame_body": {
|
||||
"full_extra": "Full extra",
|
||||
"leather_seat": "Bőrülés",
|
||||
"heated_seat": "Fűthető ülés",
|
||||
"backrest": "Háttámla",
|
||||
"center_stand": "Középsztender",
|
||||
"footrest": "Lábtartó",
|
||||
@@ -243,7 +235,6 @@ MOTORCYCLE_FEATURES: dict[str, dict[str, str]] = {
|
||||
"crash_bar": "Bukócső / bukógomba",
|
||||
"hand_guards": "Kézvédők",
|
||||
"heated_mirror": "Fűthető tükör",
|
||||
"tow_hitch": "Vonóhorog",
|
||||
},
|
||||
"luggage": {
|
||||
"factory_cases": "Gyári dobozok",
|
||||
@@ -269,15 +260,12 @@ MOTORCYCLE_FEATURES: dict[str, dict[str, str]] = {
|
||||
"showroom_vehicle": "Bemutató jármű",
|
||||
"orderable": "Rendelhető",
|
||||
"car_trade_in_possible": "Autóbeszámítás lehetséges",
|
||||
"first_owner": "Első tulajdonostól",
|
||||
"garage_kept": "Garázsban tartott",
|
||||
"female_owner": "Hölgy tulajdonostól",
|
||||
"low_mileage": "Keveset futott",
|
||||
"second_owner": "Második tulajdonostól",
|
||||
"motorcycle_trade_in_possible": "Motorbeszámítás lehetséges",
|
||||
"track_fairing": "Pályaidom",
|
||||
"regularly_maintained": "Rendszeresen karbantartott",
|
||||
"service_book": "Szervizkönyv",
|
||||
"title_certificate": "Törzskönyv",
|
||||
},
|
||||
}
|
||||
@@ -309,53 +297,22 @@ LCV_BODY_TYPES: dict[str, str] = {
|
||||
|
||||
LCV_FEATURES: dict[str, dict[str, str]] = {
|
||||
"technical": {
|
||||
"abs": "ABS",
|
||||
"asr": "ASR (Kipörgésgátló)",
|
||||
"gps_tracker": "GPS nyomkövető",
|
||||
"immobiliser": "Immobiliser",
|
||||
"alarm": "Riasztó",
|
||||
"cruise_control": "Tempomat",
|
||||
"parking_sensors_rear": "Hátsó parkolóérzékelők",
|
||||
},
|
||||
"interior": {
|
||||
"curtain_airbag": "Függönylégzsák",
|
||||
"rear_side_airbag": "Hátsó oldallégzsák",
|
||||
"switchable_airbag": "Kikapcsolható utaslégzsák",
|
||||
"side_airbag": "Oldallégzsák",
|
||||
"passenger_airbag": "Utaslégzsák",
|
||||
"driver_airbag": "Vezető légzsák",
|
||||
"roll_bar": "Bukókeret",
|
||||
"cargo_tie_down": "Rögzítő pontok a raktérben",
|
||||
"isofix": "Isofix gyerekülés rögzítés",
|
||||
"full_extra": "Full extra",
|
||||
"auxiliary_heating": "Kiegészítő fűtés (Webasto)",
|
||||
"leather_interior": "Bőr belső",
|
||||
"heated_seat": "Fűthető ülés",
|
||||
"partition_wall": "Válaszfal",
|
||||
"seat_height_adjustment": "Ülésmagasság állítás",
|
||||
"adjustable_steering_wheel": "Állítható kormány",
|
||||
"central_locking": "Központi zár",
|
||||
"trip_computer": "Fedélzeti számítógép",
|
||||
"power_steering": "Szervokormány",
|
||||
},
|
||||
"exterior": {
|
||||
"power_windows": "Elektromos ablakemelő",
|
||||
"power_mirrors": "Elektromos tükör állítás",
|
||||
"heated_mirrors": "Fűthető tükör",
|
||||
"alloy_wheels": "Könnyűfém felni",
|
||||
"tinted_glass": "Sötétített üvegek",
|
||||
"tow_bar": "Vontatóhorog",
|
||||
"sunroof": "Napfénytető",
|
||||
"fog_lights": "Ködlámpa",
|
||||
"xenon_headlights": "Xenon fényszóró",
|
||||
},
|
||||
"multimedia": {
|
||||
"cd_changer": "CD váltó",
|
||||
"cd_radio": "CD rádió",
|
||||
"gps": "GPS navigáció",
|
||||
"hifi": "Hi-Fi hangrendszer",
|
||||
"radio_cassette": "Rádiós kazettás magnó",
|
||||
},
|
||||
"multimedia": {},
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@@ -529,20 +486,16 @@ HGV_FEATURES: dict[str, dict[str, str]] = {
|
||||
"engine_brake": "Motorfék (Engine Brake)",
|
||||
"exhaust_brake": "Kipufogófék (Exhaust Brake)",
|
||||
"auxiliary_brake": "Segédfék",
|
||||
"abs": "ABS (Blokkolásgátló)",
|
||||
"ebs": "EBS (Elektronikus fékrendszer)",
|
||||
"esp": "ESP (Menetstabilizáló)",
|
||||
"traction_control": "Kipörgésgátló (ASR/TCS)",
|
||||
"roll_stability": "Borulásgátló (RSP)",
|
||||
"stability_control": "Stabilitás szabályzó (ESP)",
|
||||
"hill_holder": "Hegymeneti tartó (Hill Holder)",
|
||||
"hill_descent": "Lejtmenetvezérlő (Hill Descent)",
|
||||
"hill_descent_control": "Lejtmenetvezérlő",
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# 2. Vezetéstámogató rendszerek (ADAS)
|
||||
# ═══════════════════════════════════════════════
|
||||
"adaptive_cruise": "Adaptív tempomat (ACC)",
|
||||
"lane_departure": "Sávelhagyás figyelő (LDWS)",
|
||||
"lane_assist": "Sávtartó asszisztens",
|
||||
"blind_spot": "Holtpont-figyelő (Blind Spot)",
|
||||
@@ -552,7 +505,6 @@ HGV_FEATURES: dict[str, dict[str, str]] = {
|
||||
"emergency_brake_assist": "Vészfék asszisztens",
|
||||
"traffic_sign": "Táblafelismerő rendszer",
|
||||
"traffic_sign_recognition": "Táblafelismerő",
|
||||
"driver_alert": "Fáradtságfigyelő (Driver Alert)",
|
||||
"crosswind_assist": "Keresztszél asszisztens",
|
||||
"tyre_pressure_monitor": "Guminyomás monitor (TPMS)",
|
||||
"tire_pressure": "Guminyomás-figyelő (TPMS)",
|
||||
@@ -563,7 +515,6 @@ HGV_FEATURES: dict[str, dict[str, str]] = {
|
||||
"tachograph": "Menetíró (Tachograph)",
|
||||
"digital_tachograph": "Digitális menetíró",
|
||||
"smart_tachograph": "Smart Tachográf (G2V2)",
|
||||
"gps_tracker": "GPS nyomkövető",
|
||||
"fleet_management": "Flottamenedzsment rendszer",
|
||||
"fuel_monitoring": "Üzemanyag monitorozás",
|
||||
"fuel_card_reader": "Üzemanyagkártya-olvasó",
|
||||
@@ -576,10 +527,8 @@ HGV_FEATURES: dict[str, dict[str, str]] = {
|
||||
# 4. Látás & Kamerák
|
||||
# ═══════════════════════════════════════════════
|
||||
"reverse_camera": "Tolatókamera",
|
||||
"rear_camera": "Hátsó kamera",
|
||||
"front_camera": "Első kamera",
|
||||
"side_camera": "Oldalsó kamera",
|
||||
"360_camera": "360°-os kamera",
|
||||
"parking_sensors": "Parkolóérzékelők",
|
||||
"front_parking_sensors": "Első parkolóérzékelők",
|
||||
"rear_parking_sensors": "Hátsó parkolóérzékelők",
|
||||
@@ -589,14 +538,9 @@ HGV_FEATURES: dict[str, dict[str, str]] = {
|
||||
# 5. Kötelező tartozékok & Egyéb
|
||||
# ═══════════════════════════════════════════════
|
||||
|
||||
"fire_extinguisher": "Tűzoltó készülék",
|
||||
"first_aid_kit": "Elsősegély csomag",
|
||||
"warning_triangle": "Figyelmeztető háromszög",
|
||||
"wheel_chocks": "Ékek (kerékrögzítő)",
|
||||
"reflective_vest": "Reflexiós mellény",
|
||||
"roof_hatch": "Tetőablak",
|
||||
"roof_ladder": "Tetőlétra",
|
||||
"mudguards": "Sárvédők",
|
||||
"spare_wheel": "Pótkerék",
|
||||
"snow_chains": "Hólánc",
|
||||
"toolbox": "Szerszámos láda",
|
||||
@@ -653,9 +597,6 @@ HGV_FEATURES: dict[str, dict[str, str]] = {
|
||||
"seat_heating": "Ülésfűtés",
|
||||
"seat_ventilation": "Ülésszellőzés",
|
||||
"armrest": "Karfák",
|
||||
"adjustable_steering": "Állítható kormány",
|
||||
"electric_windows": "Elektromos ablakok",
|
||||
"central_locking": "Központi zár",
|
||||
"storage_box": "Tároló doboz",
|
||||
"wardrobe": "Gardrób / ruhatároló",
|
||||
"bed_extension": "Ágy kihúzó panel",
|
||||
@@ -667,22 +608,11 @@ HGV_FEATURES: dict[str, dict[str, str]] = {
|
||||
# MULTIMEDIA (Multimédia)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
"multimedia": {
|
||||
"navigation": "Navigáció",
|
||||
"apple_carplay": "Apple CarPlay",
|
||||
"android_auto": "Android Auto",
|
||||
"bluetooth": "Bluetooth",
|
||||
"usb_charging": "USB töltő",
|
||||
"wireless_charging": "Vezeték nélküli töltő",
|
||||
"dab_radio": "DAB+ digitális rádió",
|
||||
"cb_radio": "CB rádió",
|
||||
"satellite_radio": "Műholdas rádió",
|
||||
"digital_tv": "Digitális TV",
|
||||
"dvb_t": "DVB-T (földi digitális TV)",
|
||||
"dvb_s": "DVB-S (műholdas TV)",
|
||||
"premium_sound": "Prémium hangrendszer",
|
||||
"subwoofer": "Mélysugárzó",
|
||||
"rear_entertainment": "Hátsó szórakoztató rendszer",
|
||||
"wifi_hotspot": "WiFi hotspot",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,17 @@ from app.models import VehicleType, VehicleModelDefinition, FeatureDefinition #
|
||||
from app.models import SecurityAuditLog, OperationalLog, FinancialLedger # noqa <--- KRITIKUS!
|
||||
|
||||
from app.models import ( # noqa
|
||||
Asset, AssetCatalog, AssetCost, AssetEvent,
|
||||
AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate
|
||||
Asset, AssetCatalog, AssetEvent,
|
||||
AssetTelemetry, AssetReview, ExchangeRate
|
||||
)
|
||||
|
||||
from app.models.fleet_finance import ( # noqa
|
||||
AssetCost, CostCategory, AssetFinancials,
|
||||
InsuranceProvider, VehicleInsurancePolicy, VehicleTaxObligation
|
||||
)
|
||||
|
||||
from app.models.fleet import ( # noqa
|
||||
OrganizationRelationship, ContactPerson
|
||||
)
|
||||
|
||||
from app.models import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger # noqa
|
||||
@@ -30,6 +39,6 @@ from app.models import Document # noqa
|
||||
from app.models import Translation # noqa
|
||||
|
||||
from app.models.core_logic import ( # noqa
|
||||
SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||
SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||
)
|
||||
from app.models import PendingAction # noqa
|
||||
from app.models import PendingAction # noqa
|
||||
|
||||
@@ -8,18 +8,24 @@ from .identity.identity import Person, User, Wallet, VerificationToken, SocialAc
|
||||
# 2. Szervezeti felépítés (MUST be before Address/Rating due to FK references)
|
||||
from .marketplace.organization import Organization, OrganizationMember, OrganizationFinancials, OrganizationSalesAssignment, OrgType, OrgUserRole, OrgRole, Branch
|
||||
|
||||
# 2b. B2B kapcsolatok és kapcsolattartók (fleet schema)
|
||||
from .fleet import OrganizationRelationship, ContactPerson
|
||||
|
||||
# 3. Földrajzi adatok és címek
|
||||
from .identity.address import Address, GeoPostalCode, GeoStreet, GeoStreetType, Rating
|
||||
|
||||
# 4. Jármű definíciók
|
||||
from .vehicle.vehicle_definitions import VehicleModelDefinition, VehicleType, FeatureDefinition, ModelFeatureMap
|
||||
from .reference_data import ReferenceLookup
|
||||
from .vehicle.vehicle import CostCategory, VehicleCost, GbCatalogDiscovery
|
||||
from .vehicle.vehicle import VehicleUserRating, GbCatalogDiscovery
|
||||
from .vehicle.external_reference import ExternalReferenceLibrary
|
||||
from .vehicle.external_reference_queue import ExternalReferenceQueue
|
||||
|
||||
# 5. Eszközök és katalógusok
|
||||
from .vehicle.asset import Asset, AssetCatalog, AssetCost, AssetEvent, AssetAssignment, AssetFinancials, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership, OdometerReading
|
||||
from .vehicle.asset import Asset, AssetCatalog, AssetEvent, AssetAssignment, AssetTelemetry, AssetReview, ExchangeRate, CatalogDiscovery, VehicleOwnership, OdometerReading
|
||||
|
||||
# 5b. Flotta pénzügyek (fleet_finance schema - áthelyezve vehicle-ból)
|
||||
from .fleet_finance import AssetCost, CostCategory, AssetFinancials, InsuranceProvider, VehicleInsurancePolicy, VehicleTaxObligation
|
||||
|
||||
# 6. Üzleti logika és előfizetések
|
||||
from .core_logic import SubscriptionTier, OrganizationSubscription, CreditTransaction, ServiceSpecialty
|
||||
@@ -27,7 +33,6 @@ from .marketplace.payment import PaymentIntent, PaymentIntentStatus
|
||||
from .marketplace.finance import Issuer, IssuerType
|
||||
|
||||
# 7. Szolgáltatások és staging
|
||||
# JAVÍTVA: ServiceStaging és társai a staged_data-ból jönnek!
|
||||
from .marketplace.service import ServiceProfile, ExpertiseTag, ServiceExpertise
|
||||
from .marketplace.staged_data import ServiceStaging, DiscoveryParameter, StagedVehicleData
|
||||
from .marketplace.service_request import ServiceRequest
|
||||
@@ -38,12 +43,10 @@ from .identity.social import ServiceProvider, Vote, Competition, UserScore, Serv
|
||||
# 9. Rendszer, Gamification és egyebek
|
||||
from .gamification.gamification import PointRule, LevelConfig, UserStats, Badge, UserBadge, PointsLedger, UserContribution, Season
|
||||
|
||||
# --- 2.2 ÚJDONSÁG: InternalNotification hozzáadása ---
|
||||
from .system.system import SystemParameter, ParameterScope, InternalNotification, SystemServiceStaging
|
||||
|
||||
from .system.document import Document
|
||||
from .system.translation import Translation
|
||||
# Direct import from audit module
|
||||
from .system.audit import SecurityAuditLog, OperationalLog, ProcessLog, FinancialLedger, WalletType, LedgerStatus, LedgerEntryType
|
||||
from .vehicle.history import AuditLog, LogSeverity
|
||||
from .identity.security import PendingAction, ActionStatus
|
||||
@@ -67,30 +70,30 @@ ServiceRecord = AssetEvent
|
||||
__all__ = [
|
||||
"Base", "User", "Person", "Wallet", "UserRole", "VerificationToken", "SocialAccount",
|
||||
"Organization", "OrganizationMember", "OrganizationSalesAssignment", "OrgType", "OrgUserRole", "OrgRole",
|
||||
"OrganizationRelationship", "ContactPerson",
|
||||
"Asset", "AssetCatalog", "AssetCost", "AssetEvent", "AssetAssignment", "AssetFinancials",
|
||||
"AssetTelemetry", "AssetReview", "ExchangeRate", "CatalogDiscovery", "OdometerReading",
|
||||
"Address", "GeoPostalCode", "GeoStreet", "GeoStreetType", "Branch",
|
||||
"PointRule", "LevelConfig", "UserStats", "Badge", "UserBadge", "Rating", "PointsLedger", "UserContribution",
|
||||
|
||||
# --- 2.2 ÚJDONSÁG KIEGÉSZÍTÉS ---
|
||||
"SystemParameter", "ParameterScope", "InternalNotification",
|
||||
|
||||
# Social models (Social 3)
|
||||
"ServiceProvider", "Vote", "Competition", "UserScore", "ServiceReview", "ModerationStatus", "SourceType",
|
||||
|
||||
"Document", "Translation", "PendingAction", "ActionStatus",
|
||||
"SubscriptionTier", "OrganizationSubscription", "CreditTransaction", "ServiceSpecialty",
|
||||
"PaymentIntent", "PaymentIntentStatus",
|
||||
"AuditLog", "VehicleOwnership", "LogSeverity",
|
||||
"SecurityAuditLog", "OperationalLog", "ProcessLog",
|
||||
"SecurityAuditLog", "OperationalLog", "ProcessLog",
|
||||
"FinancialLedger", "WalletType", "LedgerStatus", "LedgerEntryType",
|
||||
"ServiceProfile", "ExpertiseTag", "ServiceExpertise", "ServiceStaging", "DiscoveryParameter", "ServiceRequest",
|
||||
"Vehicle", "UserVehicle", "VehicleCatalog", "ServiceRecord", "VehicleModelDefinition", "ReferenceLookup",
|
||||
"VehicleType", "FeatureDefinition", "ModelFeatureMap", "LegalDocument", "LegalAcceptance",
|
||||
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "VehicleCost", "ExternalReferenceLibrary", "ExternalReferenceQueue",
|
||||
"Location", "LocationType", "Issuer", "IssuerType", "CostCategory", "ExternalReferenceLibrary", "ExternalReferenceQueue",
|
||||
"GbCatalogDiscovery", "Season", "StagedVehicleData", "OneTimePassword",
|
||||
"InsuranceProvider", "VehicleInsurancePolicy", "VehicleTaxObligation",
|
||||
# Marketing / Ad Engine
|
||||
"Campaign", "Creative", "Placement", "CampaignCreative",
|
||||
"CampaignPlacement", "AdImpression", "AdClick",
|
||||
"CampaignStatus", "CreativeType", "PlacementType",
|
||||
]
|
||||
]
|
||||
|
||||
13
backend/app/models/fleet/__init__.py
Normal file
13
backend/app/models/fleet/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/fleet/__init__.py
|
||||
"""
|
||||
fleet schema: B2B kapcsolatok és kapcsolattartók.
|
||||
- OrganizationRelationship: Két szervezet közötti B2B kapcsolat (vevő, beszállító, partner)
|
||||
- ContactPerson: Szervezethez tartozó kapcsolattartó személy
|
||||
"""
|
||||
|
||||
from .organization import OrganizationRelationship, ContactPerson
|
||||
|
||||
__all__ = [
|
||||
"OrganizationRelationship",
|
||||
"ContactPerson",
|
||||
]
|
||||
146
backend/app/models/fleet/organization.py
Normal file
146
backend/app/models/fleet/organization.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/fleet/organization.py
|
||||
"""
|
||||
fleet schema: B2B kapcsolatok és kapcsolattartók.
|
||||
- OrganizationRelationship: Két szervezet közötti B2B kapcsolat (vevő, beszállító, partner)
|
||||
- ContactPerson: Szervezethez tartozó kapcsolattartó személy
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import (
|
||||
String, Boolean, DateTime, ForeignKey, Numeric, Text, Integer, BigInteger, text
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.models.identity.identity import Person
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# OrganizationRelationship
|
||||
# =============================================================================
|
||||
|
||||
class OrganizationRelationship(Base):
|
||||
"""
|
||||
Két szervezet közötti B2B kapcsolat.
|
||||
Lehetővé teszi a vevő-beszállító, partneri kapcsolatok nyilvántartását
|
||||
anélkül, hogy a szervezet rekordokat duplikálni kellene.
|
||||
"""
|
||||
__tablename__ = "org_relationships"
|
||||
__table_args__ = {"schema": "fleet"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
|
||||
# A kapcsolat két oldala
|
||||
source_org_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("fleet.organizations.id"), nullable=False, index=True
|
||||
)
|
||||
target_org_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("fleet.organizations.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# Kapcsolat típusa
|
||||
relationship_type: Mapped[str] = mapped_column(
|
||||
String(30), nullable=False, comment="CUSTOMER, SUPPLIER, PARTNER"
|
||||
)
|
||||
|
||||
# Státusz
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(20), default="ACTIVE", server_default=text("'ACTIVE'")
|
||||
)
|
||||
|
||||
# Szerződéses adatok
|
||||
contract_ref: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
valid_from: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
valid_until: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Pénzügyi feltételek
|
||||
credit_limit: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
payment_terms: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
|
||||
# Megjegyzések
|
||||
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Időbélyegek
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=func.now(), server_default=func.now()
|
||||
)
|
||||
|
||||
# Kapcsolatok
|
||||
source_org: Mapped["Organization"] = relationship(
|
||||
"Organization", foreign_keys=[source_org_id]
|
||||
)
|
||||
target_org: Mapped["Organization"] = relationship(
|
||||
"Organization", foreign_keys=[target_org_id]
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"OrganizationRelationship(id={self.id}, "
|
||||
f"source={self.source_org_id}, target={self.target_org_id}, "
|
||||
f"type='{self.relationship_type}')"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ContactPerson
|
||||
# =============================================================================
|
||||
|
||||
class ContactPerson(Base):
|
||||
"""
|
||||
Kapcsolattartó személy egy szervezethez.
|
||||
A személy rekord az identity.persons táblában él,
|
||||
ez a modell csak a szervezeti kapcsolatot és szerepkört tárolja.
|
||||
"""
|
||||
__tablename__ = "contact_persons"
|
||||
__table_args__ = {"schema": "fleet"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
|
||||
# Melyik szervezethez tartozik
|
||||
organization_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("fleet.organizations.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# Hivatkozás a személy rekordra (identity sémában)
|
||||
person_id: Mapped[int] = mapped_column(
|
||||
BigInteger, ForeignKey("identity.persons.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
# Szerepkör a szervezetben (pl. "CEO", "Fleet Manager", "Accountant")
|
||||
role: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
|
||||
# Elsődleges kapcsolattartó?
|
||||
is_primary: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"))
|
||||
|
||||
# Részleg
|
||||
department: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
|
||||
# Megjegyzések
|
||||
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Időbélyegek
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), onupdate=func.now(), server_default=func.now()
|
||||
)
|
||||
|
||||
# Kapcsolatok
|
||||
organization: Mapped["Organization"] = relationship(
|
||||
"Organization", foreign_keys=[organization_id]
|
||||
)
|
||||
person: Mapped["Person"] = relationship(
|
||||
"Person", foreign_keys=[person_id]
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"ContactPerson(id={self.id}, org_id={self.organization_id}, "
|
||||
f"person_id={self.person_id}, role='{self.role}')"
|
||||
)
|
||||
25
backend/app/models/fleet_finance/__init__.py
Normal file
25
backend/app/models/fleet_finance/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/fleet_finance/__init__.py
|
||||
"""
|
||||
fleet_finance schema: Flotta pénzügyi adatok (költségek, finanszírozás, biztosítás, adók).
|
||||
Áthelyezve a vehicle sémából: AssetCost, CostCategory, AssetFinancials.
|
||||
"""
|
||||
|
||||
from .models import (
|
||||
AssetCost,
|
||||
AssetCostStatusEnum,
|
||||
CostCategory,
|
||||
AssetFinancials,
|
||||
InsuranceProvider,
|
||||
VehicleInsurancePolicy,
|
||||
VehicleTaxObligation,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AssetCost",
|
||||
"AssetCostStatusEnum",
|
||||
"CostCategory",
|
||||
"AssetFinancials",
|
||||
"InsuranceProvider",
|
||||
"VehicleInsurancePolicy",
|
||||
"VehicleTaxObligation",
|
||||
]
|
||||
316
backend/app/models/fleet_finance/models.py
Normal file
316
backend/app/models/fleet_finance/models.py
Normal file
@@ -0,0 +1,316 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/fleet_finance/models.py
|
||||
"""
|
||||
fleet_finance schema: Flotta pénzügyi adatok.
|
||||
- CostCategory: Standardizált költségkategóriák hierarchiája (áthelyezve vehicle -> fleet_finance)
|
||||
- AssetCost: Eszközhöz kapcsolódó tényleges költségnapló (áthelyezve vehicle -> fleet_finance)
|
||||
- AssetFinancials: Beszerzés és értékcsökkenés (áthelyezve vehicle -> fleet_finance, kibővítve finanszírozási adatokkal)
|
||||
- InsuranceProvider: Biztosítók katalógusa (ÚJ)
|
||||
- VehicleInsurancePolicy: Jármű biztosítási kötvények (ÚJ)
|
||||
- VehicleTaxObligation: Jármű adókötelezettségek (ÚJ)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
import enum
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List, TYPE_CHECKING
|
||||
from sqlalchemy import (
|
||||
String, Boolean, DateTime, ForeignKey, Numeric, text, Text,
|
||||
UniqueConstraint, Integer, Date
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
||||
from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.vehicle.asset import Asset
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.models.system.document import Document
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CostCategory (áthelyezve vehicle -> fleet_finance)
|
||||
# =============================================================================
|
||||
|
||||
class CostCategory(Base):
|
||||
"""
|
||||
Standardizált költségkategóriák hierarchikus fája.
|
||||
Rendszerkategóriák (is_system=True) nem törölhetők, csak felhasználói kategóriák.
|
||||
Áthelyezve a vehicle sémából a fleet_finance sémába.
|
||||
"""
|
||||
__tablename__ = "cost_categories"
|
||||
__table_args__ = {"schema": "fleet_finance", "extend_existing": True}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
parent_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("fleet_finance.cost_categories.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
code: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
|
||||
visibility: Mapped[str] = mapped_column(String(20), default="both", server_default="'both'")
|
||||
min_tier: Mapped[str] = mapped_column(String(20), default="free", server_default="'free'")
|
||||
accounting_code: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||
|
||||
# Hierarchikus kapcsolatok
|
||||
parent: Mapped[Optional["CostCategory"]] = relationship(
|
||||
"CostCategory",
|
||||
remote_side=[id],
|
||||
back_populates="children",
|
||||
foreign_keys=[parent_id]
|
||||
)
|
||||
children: Mapped[list["CostCategory"]] = relationship(
|
||||
"CostCategory",
|
||||
back_populates="parent",
|
||||
foreign_keys=[parent_id]
|
||||
)
|
||||
|
||||
# Kapcsolódó költségek (AssetCost)
|
||||
asset_costs: Mapped[list["AssetCost"]] = relationship("AssetCost", back_populates="category")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CostCategory(id={self.id}, code='{self.code}', name='{self.name}')"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetCostStatusEnum
|
||||
# =============================================================================
|
||||
|
||||
class AssetCostStatusEnum(str, enum.Enum):
|
||||
"""Költség státuszok a jóváhagyási munkafolyamathoz."""
|
||||
DRAFT = "DRAFT"
|
||||
PENDING_APPROVAL = "PENDING_APPROVAL"
|
||||
APPROVED = "APPROVED"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetCost (áthelyezve vehicle -> fleet_finance)
|
||||
# =============================================================================
|
||||
|
||||
class AssetCost(Base):
|
||||
""" II. Üzemeltetés és TCO kimutatás. Áthelyezve a vehicle sémából. """
|
||||
__tablename__ = "asset_costs"
|
||||
__table_args__ = {"schema": "fleet_finance"}
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False)
|
||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
|
||||
|
||||
category_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet_finance.cost_categories.id"), index=True, nullable=False)
|
||||
amount_net: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||
amount_gross: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
vat_rate: Mapped[Optional[float]] = mapped_column(Numeric(5, 2), nullable=True)
|
||||
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
invoice_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.documents.id"), nullable=True)
|
||||
|
||||
# Státusz a jóváhagyási munkafolyamathoz
|
||||
status: Mapped[str] = mapped_column(String(30), default="DRAFT", server_default=text("'DRAFT'"), index=True)
|
||||
|
||||
# Kétirányú hivatkozás az AssetEvent felé (kettős könyvelés elkerülése)
|
||||
linked_asset_event_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
PG_UUID(as_uuid=True),
|
||||
ForeignKey("vehicle.asset_events.id"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
|
||||
# === BESZÁLLÍTÓI HIVATKOZÁSOK (B2B expansion) ===
|
||||
# Hivatkozás a fleet.organizations táblában lévő beszállítóra
|
||||
vendor_organization_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("fleet.organizations.id"), nullable=True, index=True
|
||||
)
|
||||
# Szabadon gépelhető beszállító név (ha nincs a rendszerben)
|
||||
external_vendor_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||
|
||||
# === KÖNYVELÉSI DÁTUMOK ===
|
||||
invoice_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
fulfillment_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
|
||||
data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="costs")
|
||||
organization: Mapped["Organization"] = relationship("Organization", foreign_keys=[organization_id])
|
||||
category: Mapped["CostCategory"] = relationship("CostCategory", back_populates="asset_costs")
|
||||
|
||||
# Kapcsolat a hivatkozott eseményhez
|
||||
linked_event: Mapped[Optional["AssetEvent"]] = relationship(
|
||||
"AssetEvent",
|
||||
foreign_keys=[linked_asset_event_id],
|
||||
post_update=True
|
||||
)
|
||||
|
||||
# Beszállító kapcsolat
|
||||
vendor_organization: Mapped[Optional["Organization"]] = relationship(
|
||||
"Organization", foreign_keys=[vendor_organization_id]
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetFinancials (áthelyezve vehicle -> fleet_finance, kibővítve)
|
||||
# =============================================================================
|
||||
|
||||
class AssetFinancials(Base):
|
||||
"""
|
||||
I. Beszerzés és IV. Értékcsökkenés (Amortizáció).
|
||||
Áthelyezve a vehicle sémából a fleet_finance sémába.
|
||||
Kibővítve finanszírozási adatokkal (lízing, hitel).
|
||||
"""
|
||||
__tablename__ = "asset_financials"
|
||||
__table_args__ = {"schema": "fleet_finance"}
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), unique=True)
|
||||
|
||||
# === BESZERZÉS ===
|
||||
purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
|
||||
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
verified_purchase_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
financing_type: Mapped[str] = mapped_column(String(50))
|
||||
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
# === FINANSZÍROZÁS (ÚJ) ===
|
||||
down_payment: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
monthly_installment: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
residual_value: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
contract_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
financing_provider: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
|
||||
interest_rate: Mapped[Optional[float]] = mapped_column(Numeric(5, 2), nullable=True)
|
||||
total_contract_value: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
lease_start_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
lease_end_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
|
||||
# === KAPCSOLATOK ===
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# InsuranceProvider (ÚJ)
|
||||
# =============================================================================
|
||||
|
||||
class InsuranceProvider(Base):
|
||||
"""
|
||||
Biztosítók katalógusa.
|
||||
Tárolja a biztosítók elérhetőségeit és szolgáltatásait.
|
||||
"""
|
||||
__tablename__ = "insurance_providers"
|
||||
__table_args__ = {"schema": "fleet_finance"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
name: Mapped[str] = mapped_column(String(150), nullable=False, index=True)
|
||||
country_code: Mapped[Optional[str]] = mapped_column(String(2), index=True, nullable=True, comment="ISO 3166-1 alpha-2 országkód (pl. 'HU'). Ha NULL, akkor globális.")
|
||||
claim_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
claim_url: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
||||
services_offered: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
|
||||
|
||||
# Kapcsolatok
|
||||
policies: Mapped[List["VehicleInsurancePolicy"]] = relationship(
|
||||
"VehicleInsurancePolicy", back_populates="provider"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"InsuranceProvider(id={self.id}, name='{self.name}')"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VehicleInsurancePolicy (ÚJ)
|
||||
# =============================================================================
|
||||
|
||||
class VehicleInsurancePolicy(Base):
|
||||
"""
|
||||
Jármű biztosítási kötvények.
|
||||
Minden kötvény egy járműhöz (asset) és egy biztosítóhoz (provider) tartozik.
|
||||
"""
|
||||
__tablename__ = "vehicle_insurance_policies"
|
||||
__table_args__ = {"schema": "fleet_finance"}
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
asset_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False, index=True
|
||||
)
|
||||
provider_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("fleet_finance.insurance_providers.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
insurance_type: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, comment="KGFB, CASCO"
|
||||
)
|
||||
policy_number: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||
start_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
expiry_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
premium_amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||
|
||||
# Dokumentum kapcsolat (PDF kötvény)
|
||||
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
PG_UUID(as_uuid=True), ForeignKey("system.documents.id"), nullable=True
|
||||
)
|
||||
|
||||
# Kapcsolatok
|
||||
asset: Mapped["Asset"] = relationship("Asset")
|
||||
provider: Mapped["InsuranceProvider"] = relationship(
|
||||
"InsuranceProvider", back_populates="policies"
|
||||
)
|
||||
document: Mapped[Optional["Document"]] = relationship("Document")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"VehicleInsurancePolicy(id={self.id}, asset_id={self.asset_id}, "
|
||||
f"type='{self.insurance_type}', policy='{self.policy_number}')"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VehicleTaxObligation (ÚJ)
|
||||
# =============================================================================
|
||||
|
||||
class VehicleTaxObligation(Base):
|
||||
"""
|
||||
Jármű adókötelezettségek.
|
||||
Tárolja a különböző típusú adókat (súlyadó, cégautóadó, stb.).
|
||||
"""
|
||||
__tablename__ = "vehicle_tax_obligations"
|
||||
__table_args__ = {"schema": "fleet_finance"}
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
asset_id: Mapped[uuid.UUID] = mapped_column(
|
||||
PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False, index=True
|
||||
)
|
||||
|
||||
tax_type: Mapped[str] = mapped_column(
|
||||
String(30), nullable=False, comment="WEIGHT_TAX, COMPANY_CAR_TAX"
|
||||
)
|
||||
tax_year: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
|
||||
payment_status: Mapped[str] = mapped_column(
|
||||
String(20), default="pending", server_default=text("'pending'")
|
||||
)
|
||||
|
||||
# Dokumentum kapcsolat (PDF adóbevallás)
|
||||
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
PG_UUID(as_uuid=True), ForeignKey("system.documents.id"), nullable=True
|
||||
)
|
||||
|
||||
# Kapcsolatok
|
||||
asset: Mapped["Asset"] = relationship("Asset")
|
||||
document: Mapped[Optional["Document"]] = relationship("Document")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"VehicleTaxObligation(id={self.id}, asset_id={self.asset_id}, "
|
||||
f"type='{self.tax_type}', year={self.tax_year})"
|
||||
)
|
||||
@@ -139,6 +139,7 @@ class Organization(Base):
|
||||
|
||||
# ── 📦 ELŐFIZETÉS / SUBSCRIPTION ──
|
||||
subscription_plan: Mapped[str] = mapped_column(String(30), server_default=text("'FREE'"), index=True)
|
||||
subscription_expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
||||
base_asset_limit: Mapped[int] = mapped_column(Integer, server_default=text("1"))
|
||||
purchased_extra_slots: Mapped[int] = mapped_column(Integer, server_default=text("0"))
|
||||
|
||||
@@ -208,8 +209,6 @@ class Organization(Base):
|
||||
# Kapcsolat az örök személy rekordhoz
|
||||
legal_owner: Mapped[Optional["Person"]] = relationship("Person", back_populates="owned_business_entities")
|
||||
|
||||
# Kapcsolat a jármű költségekhez (TCO rendszer)
|
||||
vehicle_costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="organization")
|
||||
|
||||
class OrganizationFinancials(Base):
|
||||
__tablename__ = "organization_financials"
|
||||
|
||||
@@ -8,8 +8,6 @@ from .vehicle_definitions import (
|
||||
)
|
||||
|
||||
from .vehicle import (
|
||||
CostCategory,
|
||||
VehicleCost,
|
||||
VehicleUserRating,
|
||||
GbCatalogDiscovery,
|
||||
)
|
||||
@@ -19,15 +17,17 @@ from .external_reference_queue import ExternalReferenceQueue
|
||||
from .asset import (
|
||||
Asset,
|
||||
AssetCatalog,
|
||||
AssetCost,
|
||||
AssetEvent,
|
||||
AssetFinancials,
|
||||
AssetAssignment,
|
||||
AssetTelemetry,
|
||||
AssetReview,
|
||||
ExchangeRate,
|
||||
CatalogDiscovery,
|
||||
VehicleOwnership,
|
||||
OdometerReading,
|
||||
AssetEventTypeEnum,
|
||||
AssetEventStatusEnum,
|
||||
VehicleTransferRequest,
|
||||
)
|
||||
|
||||
from .history import AuditLog, LogSeverity
|
||||
@@ -40,17 +40,14 @@ __all__ = [
|
||||
"VehicleType",
|
||||
"FeatureDefinition",
|
||||
"ModelFeatureMap",
|
||||
"CostCategory",
|
||||
"VehicleCost",
|
||||
"VehicleUserRating",
|
||||
"GbCatalogDiscovery",
|
||||
"ExternalReferenceLibrary",
|
||||
"ExternalReferenceQueue",
|
||||
"Asset",
|
||||
"AssetCatalog",
|
||||
"AssetCost",
|
||||
"AssetEvent",
|
||||
"AssetFinancials",
|
||||
"AssetAssignment",
|
||||
"AssetTelemetry",
|
||||
"AssetReview",
|
||||
"ExchangeRate",
|
||||
@@ -58,8 +55,10 @@ __all__ = [
|
||||
"VehicleOwnership",
|
||||
"AuditLog",
|
||||
"LogSeverity",
|
||||
# --- EXPORT LISTA KIEGÉSZÍTÉSE ---
|
||||
"OdometerReading",
|
||||
"AssetEventTypeEnum",
|
||||
"AssetEventStatusEnum",
|
||||
"VehicleTransferRequest",
|
||||
"MotorcycleSpecs",
|
||||
"BodyTypeDictionary",
|
||||
"OdometerReading",
|
||||
]
|
||||
]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
from __future__ import annotations
|
||||
import uuid
|
||||
import enum
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, CheckConstraint, BigInteger, Integer, Float
|
||||
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Numeric, text, Text, UniqueConstraint, CheckConstraint, BigInteger, Integer, Float, Date
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
|
||||
from sqlalchemy.sql import func
|
||||
@@ -129,6 +129,46 @@ class Asset(Base):
|
||||
is_under_warranty: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text('false'))
|
||||
warranty_expiry_date: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# === REGISTRATION DOCUMENTS ===
|
||||
registration_certificate_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(50), nullable=True, default=None,
|
||||
comment="Forgalmi engedély száma"
|
||||
)
|
||||
registration_certificate_validity: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, default=None,
|
||||
comment="Forgalmi engedély érvényességi ideje"
|
||||
)
|
||||
vehicle_registration_document_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(50), nullable=True, default=None,
|
||||
comment="Törzskönyv száma"
|
||||
)
|
||||
|
||||
# === INTERNATIONALIZATION (ÚJ) ===
|
||||
registration_country: Mapped[Optional[str]] = mapped_column(
|
||||
String(2), nullable=True, index=True,
|
||||
comment="Regisztráció országa (ISO 3166-1 alpha-2, pl. 'HU', 'SK', 'DE')"
|
||||
)
|
||||
first_domestic_registration_date: Mapped[Optional[date]] = mapped_column(
|
||||
Date, nullable=True,
|
||||
comment="Első belföldi forgalomba helyezés dátuma"
|
||||
)
|
||||
import_country: Mapped[Optional[str]] = mapped_column(
|
||||
String(2), nullable=True,
|
||||
comment="Importország (ISO 3166-1 alpha-2), ha importált jármű"
|
||||
)
|
||||
title_document_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(50), nullable=True,
|
||||
comment="Törzskönyv szám (title document)"
|
||||
)
|
||||
engine_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(100), nullable=True,
|
||||
comment="Motorszám"
|
||||
)
|
||||
number_of_previous_owners: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True,
|
||||
comment="Előző tulajdonosok száma"
|
||||
)
|
||||
|
||||
# === SALES MODULE ===
|
||||
is_for_sale: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||
price: Mapped[Optional[float]] = mapped_column(Numeric(15, 2))
|
||||
@@ -147,7 +187,9 @@ class Asset(Base):
|
||||
|
||||
# --- KAPCSOLATOK ---
|
||||
catalog: Mapped["AssetCatalog"] = relationship("AssetCatalog", back_populates="assets")
|
||||
financials: Mapped[Optional["AssetFinancials"]] = relationship("AssetFinancials", back_populates="asset", uselist=False)
|
||||
financials: Mapped[Optional["AssetFinancials"]] = relationship(
|
||||
"AssetFinancials", back_populates="asset", uselist=False
|
||||
)
|
||||
costs: Mapped[List["AssetCost"]] = relationship("AssetCost", back_populates="asset")
|
||||
events: Mapped[List["AssetEvent"]] = relationship("AssetEvent", back_populates="asset")
|
||||
logbook: Mapped[List["VehicleLogbook"]] = relationship("VehicleLogbook", back_populates="asset")
|
||||
@@ -229,71 +271,6 @@ class Asset(Base):
|
||||
return min(total_score, 100)
|
||||
|
||||
|
||||
class AssetFinancials(Base):
|
||||
""" I. Beszerzés és IV. Értékcsökkenés (Amortizáció). """
|
||||
__tablename__ = "asset_financials"
|
||||
__table_args__ = {"schema": "vehicle"}
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), unique=True)
|
||||
|
||||
purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
|
||||
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
|
||||
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
verified_purchase_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
|
||||
financing_type: Mapped[str] = mapped_column(String(50))
|
||||
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
|
||||
|
||||
|
||||
class AssetCostStatusEnum(str, enum.Enum):
|
||||
"""Költség státuszok a jóváhagyási munkafolyamathoz."""
|
||||
DRAFT = "DRAFT"
|
||||
PENDING_APPROVAL = "PENDING_APPROVAL"
|
||||
APPROVED = "APPROVED"
|
||||
|
||||
|
||||
class AssetCost(Base):
|
||||
""" II. Üzemeltetés és TCO kimutatás. """
|
||||
__tablename__ = "asset_costs"
|
||||
__table_args__ = {"schema": "vehicle"}
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False)
|
||||
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
|
||||
|
||||
category_id: Mapped[int] = mapped_column(Integer, ForeignKey("vehicle.cost_categories.id"), index=True, nullable=False)
|
||||
amount_net: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||
amount_gross: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
|
||||
vat_rate: Mapped[Optional[float]] = mapped_column(Numeric(5, 2), nullable=True)
|
||||
currency: Mapped[str] = mapped_column(String(3), default="HUF")
|
||||
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
invoice_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
|
||||
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.documents.id"), nullable=True)
|
||||
|
||||
# Státusz a jóváhagyási munkafolyamathoz
|
||||
status: Mapped[str] = mapped_column(String(30), default="DRAFT", server_default=text("'DRAFT'"), index=True)
|
||||
|
||||
# Kétirányú hivatkozás az AssetEvent felé (kettős könyvelés elkerülése)
|
||||
linked_asset_event_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
PG_UUID(as_uuid=True),
|
||||
ForeignKey("vehicle.asset_events.id"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
|
||||
data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="costs")
|
||||
organization: Mapped["Organization"] = relationship("Organization")
|
||||
category: Mapped["CostCategory"] = relationship("CostCategory")
|
||||
|
||||
# Kapcsolat a hivatkozott eseményhez
|
||||
linked_event: Mapped[Optional["AssetEvent"]] = relationship(
|
||||
"AssetEvent",
|
||||
foreign_keys=[linked_asset_event_id],
|
||||
post_update=True
|
||||
)
|
||||
|
||||
|
||||
class VehicleLogbook(Base):
|
||||
""" Útnyilvántartás (NAV, Kiküldetés, Munkábajárás). """
|
||||
@@ -391,11 +368,14 @@ class OdometerReading(Base):
|
||||
reading: Mapped[int] = mapped_column(Integer, nullable=False) # km óra állás
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
source: Mapped[str] = mapped_column(String(30), default="manual") # manual, api, telemetry
|
||||
cost_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.asset_costs.id"), nullable=True)
|
||||
cost_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("fleet_finance.asset_costs.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
asset: Mapped["Asset"] = relationship("Asset", back_populates="odometer_readings")
|
||||
cost: Mapped[Optional["AssetCost"]] = relationship("AssetCost")
|
||||
cost: Mapped[Optional["AssetCost"]] = relationship(
|
||||
"AssetCost",
|
||||
primaryjoin="OdometerReading.cost_id == AssetCost.id"
|
||||
)
|
||||
|
||||
|
||||
class AssetAssignment(Base):
|
||||
@@ -443,7 +423,7 @@ class AssetEvent(Base):
|
||||
event_type: Mapped[str] = mapped_column(String(50), nullable=False) # AssetEventTypeEnum értékek
|
||||
odometer_reading: Mapped[Optional[int]] = mapped_column(Integer) # Km óra állás az eseménykor
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
cost_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.asset_costs.id"), nullable=True)
|
||||
cost_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("fleet_finance.asset_costs.id"), nullable=True)
|
||||
|
||||
# Státusz a feldolgozási munkafolyamathoz
|
||||
status: Mapped[str] = mapped_column(String(30), default="DRAFT", server_default=text("'DRAFT'"), index=True)
|
||||
@@ -451,7 +431,7 @@ class AssetEvent(Base):
|
||||
# Kétirányú hivatkozás az AssetCost felé (kettős könyvelés elkerülése)
|
||||
linked_expense_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||
PG_UUID(as_uuid=True),
|
||||
ForeignKey("vehicle.asset_costs.id"),
|
||||
ForeignKey("fleet_finance.asset_costs.id"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
@@ -511,23 +491,6 @@ class CatalogDiscovery(Base):
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
|
||||
class VehicleExpenses(Base):
|
||||
""" Jármű költségek a jelentésekhez. """
|
||||
__tablename__ = "vehicle_expenses"
|
||||
__table_args__ = {"schema": "vehicle"}
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
vehicle_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False, index=True)
|
||||
category: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
||||
amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
|
||||
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationship
|
||||
asset: Mapped["Asset"] = relationship("Asset")
|
||||
|
||||
|
||||
class VehicleTransferRequest(Base):
|
||||
"""Járműátadási kérelem - asset átruházás másik tulajdonosnak vagy szervezetnek."""
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/models/vehicle/vehicle.py
|
||||
"""
|
||||
TCO (Total Cost of Ownership) alapmodelljei a 'vehicle' sémában.
|
||||
- CostCategory: Standardizált költségkategóriák hierarchiája
|
||||
- VehicleCost: Járműhöz kapcsolódó tényleges költségnapló
|
||||
- CostCategory és VehicleCost áthelyezve a fleet_finance sémába.
|
||||
- Itt marad: VehicleUserRating, GbCatalogDiscovery
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,99 +16,6 @@ from sqlalchemy.sql import func
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class CostCategory(Base):
|
||||
"""
|
||||
Standardizált költségkategóriák hierarchikus fája.
|
||||
Rendszerkategóriák (is_system=True) nem törölhetők, csak felhasználói kategóriák.
|
||||
"""
|
||||
__tablename__ = "cost_categories"
|
||||
__table_args__ = {"schema": "vehicle", "extend_existing": True}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
parent_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("vehicle.cost_categories.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
code: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
|
||||
visibility: Mapped[str] = mapped_column(String(20), default="both", server_default="'both'")
|
||||
min_tier: Mapped[str] = mapped_column(String(20), default="free", server_default="'free'") # free, premium, enterprise
|
||||
accounting_code: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||
|
||||
# Hierarchikus kapcsolatok
|
||||
parent: Mapped[Optional["CostCategory"]] = relationship(
|
||||
"CostCategory",
|
||||
remote_side=[id],
|
||||
back_populates="children",
|
||||
foreign_keys=[parent_id]
|
||||
)
|
||||
children: Mapped[list["CostCategory"]] = relationship(
|
||||
"CostCategory",
|
||||
back_populates="parent",
|
||||
foreign_keys=[parent_id]
|
||||
)
|
||||
|
||||
# Kapcsolódó költségek
|
||||
costs: Mapped[list["VehicleCost"]] = relationship("VehicleCost", back_populates="category")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CostCategory(id={self.id}, code='{self.code}', name='{self.name}')"
|
||||
|
||||
|
||||
class VehicleCost(Base):
|
||||
"""
|
||||
Járműhöz kapcsolódó tényleges költségnapló.
|
||||
Minden költséghez kötelező az odometer állás (km) és a dátum.
|
||||
Az organization_id az Univerzális Flotta hivatkozás (fleet.organizations).
|
||||
"""
|
||||
__tablename__ = "costs"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("vehicle_id", "category_id", "date", "odometer", name="uq_cost_unique_entry"),
|
||||
{"schema": "vehicle"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
vehicle_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("vehicle.vehicle_model_definitions.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
organization_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("fleet.organizations.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True
|
||||
)
|
||||
category_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("vehicle.cost_categories.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
index=True
|
||||
)
|
||||
amount: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False) # Összeg
|
||||
currency: Mapped[str] = mapped_column(String(3), default="HUF", server_default="'HUF'") # ISO valutakód
|
||||
odometer: Mapped[int] = mapped_column(Integer, nullable=False) # Kilométeróra állás (km)
|
||||
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
notes: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
|
||||
|
||||
# Kapcsolatok
|
||||
vehicle: Mapped["VehicleModelDefinition"] = relationship("VehicleModelDefinition", back_populates="costs")
|
||||
organization: Mapped[Optional["Organization"]] = relationship("Organization", back_populates="vehicle_costs")
|
||||
category: Mapped["CostCategory"] = relationship("CostCategory", back_populates="costs")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"VehicleCost(id={self.id}, vehicle_id={self.vehicle_id}, amount={self.amount} {self.currency})"
|
||||
|
||||
|
||||
class VehicleUserRating(Base):
|
||||
"""
|
||||
Jármű értékelési rendszer - User -> Vehicle kapcsolat.
|
||||
@@ -173,4 +80,4 @@ class GbCatalogDiscovery(Base):
|
||||
make: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(20), default='pending')
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
@@ -12,7 +12,7 @@ from app.database import Base
|
||||
# Típus ellenőrzés a körkörös importok elkerülésére
|
||||
if TYPE_CHECKING:
|
||||
from .asset import AssetCatalog
|
||||
from .vehicle import VehicleCost, VehicleUserRating
|
||||
from .vehicle import VehicleUserRating
|
||||
|
||||
class VehicleType(Base):
|
||||
""" Jármű kategóriák (pl. Személyautó, Motorkerékpár, Teherautó, Hajó) """
|
||||
@@ -142,7 +142,6 @@ class VehicleModelDefinition(Base):
|
||||
# JAVÍTÁS: Ez a sor hiányzott az API indításához!
|
||||
ratings: Mapped[List["VehicleUserRating"]] = relationship("VehicleUserRating", back_populates="vehicle", cascade="all, delete-orphan")
|
||||
|
||||
costs: Mapped[List["VehicleCost"]] = relationship("VehicleCost", back_populates="vehicle", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class ModelFeatureMap(Base):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field, validator, root_validator, model_validator, field_validator
|
||||
from typing import Optional, Dict, Any, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
|
||||
class AssetCatalogResponse(BaseModel):
|
||||
""" A technikai katalógus (Master Data) teljes adattartalma. """
|
||||
@@ -86,6 +86,19 @@ class AssetResponse(BaseModel):
|
||||
is_under_warranty: bool = Field(default=False, description="Érvényes jótállás")
|
||||
warranty_expiry_date: Optional[datetime] = Field(None, description="Jótállás érvényességi dátuma")
|
||||
|
||||
# === REGISTRATION DOCUMENTS ===
|
||||
registration_certificate_number: Optional[str] = Field(None, max_length=50, description="Forgalmi engedély száma")
|
||||
registration_certificate_validity: Optional[datetime] = Field(None, description="Forgalmi engedély érvényességi ideje")
|
||||
vehicle_registration_document_number: Optional[str] = Field(None, max_length=50, description="Törzskönyv száma")
|
||||
|
||||
# === INTERNATIONALIZATION (ÚJ) ===
|
||||
registration_country: Optional[str] = Field(None, min_length=2, max_length=2, description="Regisztráció országa (ISO 3166-1 alpha-2, pl. 'HU', 'SK', 'DE')")
|
||||
first_domestic_registration_date: Optional[date] = Field(None, description="Első belföldi forgalomba helyezés dátuma")
|
||||
import_country: Optional[str] = Field(None, min_length=2, max_length=2, description="Importország (ISO 3166-1 alpha-2), ha importált jármű")
|
||||
title_document_number: Optional[str] = Field(None, max_length=50, description="Törzskönyv szám (title document)")
|
||||
engine_number: Optional[str] = Field(None, max_length=100, description="Motorszám")
|
||||
number_of_previous_owners: Optional[int] = Field(None, ge=0, description="Előző tulajdonosok száma")
|
||||
|
||||
# === SALES MODULE ===
|
||||
is_for_sale: bool = Field(default=False)
|
||||
price: Optional[float] = None
|
||||
@@ -228,12 +241,53 @@ class AssetCreate(BaseModel):
|
||||
is_under_warranty: bool = Field(default=False, description="Érvényes jótállás")
|
||||
warranty_expiry_date: Optional[datetime] = Field(None, description="Jótállás érvényességi dátuma")
|
||||
|
||||
# === REGISTRATION DOCUMENTS ===
|
||||
registration_certificate_number: Optional[str] = Field(None, max_length=50, description="Forgalmi engedély száma")
|
||||
registration_certificate_validity: Optional[datetime] = Field(None, description="Forgalmi engedély érvényességi ideje")
|
||||
vehicle_registration_document_number: Optional[str] = Field(None, max_length=50, description="Törzskönyv száma")
|
||||
|
||||
# === INTERNATIONALIZATION (ÚJ) ===
|
||||
registration_country: Optional[str] = Field(None, min_length=2, max_length=2, description="Regisztráció országa (ISO 3166-1 alpha-2, pl. 'HU', 'SK', 'DE')")
|
||||
first_domestic_registration_date: Optional[date] = Field(None, description="Első belföldi forgalomba helyezés dátuma")
|
||||
import_country: Optional[str] = Field(None, min_length=2, max_length=2, description="Importország (ISO 3166-1 alpha-2), ha importált jármű")
|
||||
title_document_number: Optional[str] = Field(None, max_length=50, description="Törzskönyv szám (title document)")
|
||||
engine_number: Optional[str] = Field(None, max_length=100, description="Motorszám")
|
||||
number_of_previous_owners: Optional[int] = Field(None, ge=0, description="Előző tulajdonosok száma")
|
||||
|
||||
# === PRIMARY VEHICLE FLAG ===
|
||||
is_primary: bool = Field(default=False, description="Elsődleges jármű (tárolva: individual_equipment JSONB)")
|
||||
|
||||
# === DATA STATUS (for transfer/duplicate scenarios) ===
|
||||
data_status: Optional[str] = Field(None, description="Adat státusz (pl. 'draft' tulajdonosváltáskor)")
|
||||
|
||||
# ── Internationalization validators ──
|
||||
@field_validator('registration_country', 'import_country')
|
||||
@classmethod
|
||||
def validate_iso_country(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""registration_country és import_country: pontosan 2 karakteres nagybetűs string (ISO 3166-1 alpha-2)."""
|
||||
if v is None or not isinstance(v, str):
|
||||
return v
|
||||
cleaned = v.strip().upper()
|
||||
if len(cleaned) != 2:
|
||||
raise ValueError(f'Az országkódnak pontosan 2 karakter hosszúnak kell lennie (ISO 3166-1 alpha-2), kapott: "{v}"')
|
||||
return cleaned
|
||||
|
||||
@field_validator('number_of_previous_owners')
|
||||
@classmethod
|
||||
def validate_previous_owners(cls, v: Optional[int]) -> Optional[int]:
|
||||
"""number_of_previous_owners >= 0."""
|
||||
if v is not None and v < 0:
|
||||
raise ValueError('Az előző tulajdonosok száma nem lehet negatív')
|
||||
return v
|
||||
|
||||
@field_validator('first_domestic_registration_date')
|
||||
@classmethod
|
||||
def validate_not_future_date(cls, v: Optional[date]) -> Optional[date]:
|
||||
"""first_domestic_registration_date nem lehet jövőbeli dátum."""
|
||||
if v is not None and v > date.today():
|
||||
raise ValueError('Az első belföldi forgalomba helyezés dátuma nem lehet jövőbeli')
|
||||
return v
|
||||
|
||||
# === STATUS VALIDATION ===
|
||||
@validator('status', pre=True, always=True)
|
||||
def determine_status(cls, v, values):
|
||||
@@ -370,6 +424,47 @@ class AssetUpdate(BaseModel):
|
||||
is_under_warranty: Optional[bool] = Field(None, description="Érvényes jótállás")
|
||||
warranty_expiry_date: Optional[datetime] = Field(None, description="Jótállás érvényességi dátuma")
|
||||
|
||||
# === REGISTRATION DOCUMENTS ===
|
||||
registration_certificate_number: Optional[str] = Field(None, max_length=50, description="Forgalmi engedély száma")
|
||||
registration_certificate_validity: Optional[datetime] = Field(None, description="Forgalmi engedély érvényességi ideje")
|
||||
vehicle_registration_document_number: Optional[str] = Field(None, max_length=50, description="Törzskönyv száma")
|
||||
|
||||
# === INTERNATIONALIZATION (ÚJ) ===
|
||||
registration_country: Optional[str] = Field(None, min_length=2, max_length=2, description="Regisztráció országa (ISO 3166-1 alpha-2, pl. 'HU', 'SK', 'DE')")
|
||||
first_domestic_registration_date: Optional[date] = Field(None, description="Első belföldi forgalomba helyezés dátuma")
|
||||
import_country: Optional[str] = Field(None, min_length=2, max_length=2, description="Importország (ISO 3166-1 alpha-2), ha importált jármű")
|
||||
title_document_number: Optional[str] = Field(None, max_length=50, description="Törzskönyv szám (title document)")
|
||||
engine_number: Optional[str] = Field(None, max_length=100, description="Motorszám")
|
||||
number_of_previous_owners: Optional[int] = Field(None, ge=0, description="Előző tulajdonosok száma")
|
||||
|
||||
# ── Internationalization validators ──
|
||||
@field_validator('registration_country', 'import_country')
|
||||
@classmethod
|
||||
def validate_iso_country(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""registration_country és import_country: pontosan 2 karakteres nagybetűs string (ISO 3166-1 alpha-2)."""
|
||||
if v is None or not isinstance(v, str):
|
||||
return v
|
||||
cleaned = v.strip().upper()
|
||||
if len(cleaned) != 2:
|
||||
raise ValueError(f'Az országkódnak pontosan 2 karakter hosszúnak kell lennie (ISO 3166-1 alpha-2), kapott: "{v}"')
|
||||
return cleaned
|
||||
|
||||
@field_validator('number_of_previous_owners')
|
||||
@classmethod
|
||||
def validate_previous_owners(cls, v: Optional[int]) -> Optional[int]:
|
||||
"""number_of_previous_owners >= 0."""
|
||||
if v is not None and v < 0:
|
||||
raise ValueError('Az előző tulajdonosok száma nem lehet negatív')
|
||||
return v
|
||||
|
||||
@field_validator('first_domestic_registration_date')
|
||||
@classmethod
|
||||
def validate_not_future_date(cls, v: Optional[date]) -> Optional[date]:
|
||||
"""first_domestic_registration_date nem lehet jövőbeli dátum."""
|
||||
if v is not None and v > date.today():
|
||||
raise ValueError('Az első belföldi forgalomba helyezés dátuma nem lehet jövőbeli')
|
||||
return v
|
||||
|
||||
# === STATUS ===
|
||||
current_mileage: Optional[int] = Field(None, ge=0, description="Aktuális km óra állás")
|
||||
is_primary: Optional[bool] = Field(None, description="Elsődleges jármű")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/schemas/asset_cost.py
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
@@ -34,13 +34,21 @@ class AssetCostBase(BaseModel):
|
||||
return data
|
||||
vat_rate: Optional[Decimal] = Field(None, description="ÁFA kulcs (pl. 27.00)")
|
||||
currency: str = "HUF"
|
||||
date: datetime = Field(default_factory=datetime.now)
|
||||
cost_date: datetime = Field(default_factory=lambda: datetime.now(), validation_alias="date", description="Költség rögzítésének dátuma/időpontja")
|
||||
invoice_number: Optional[str] = None
|
||||
data: Dict[str, Any] = Field(default_factory=dict) # nyugta adatai, GPS koordináták
|
||||
category_id: int # FK a CostCategory táblához
|
||||
mileage_at_cost: Optional[int] = None # Stored inside data JSONB
|
||||
description: Optional[str] = None # Stored inside data JSONB
|
||||
|
||||
# === BESZÁLLÍTÓI ADATOK (B2B expansion) ===
|
||||
vendor_organization_id: Optional[int] = Field(None, description="Beszállító szervezet ID (FK fleet.organizations)")
|
||||
external_vendor_name: Optional[str] = Field(None, max_length=200, description="Szabadon gépelhető beszállító név (ha nincs a rendszerben)")
|
||||
|
||||
# === KÖNYVELÉSI DÁTUMOK ===
|
||||
invoice_date: Optional[date] = Field(None, description="Számla kiállításának dátuma")
|
||||
fulfillment_date: Optional[date] = Field(None, description="Teljesítés dátuma")
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_gross_first(self):
|
||||
"""Gross-First validation: ensure amount_gross is the primary source.
|
||||
@@ -86,5 +94,8 @@ class AssetCostResponse(AssetCostBase):
|
||||
category_code: Optional[str] = None # Resolved from CostCategory relationship
|
||||
description: Optional[str] = None # Extracted from data['description']
|
||||
mileage_at_cost: Optional[int] = None # Extracted from data['mileage_at_cost']
|
||||
|
||||
# Enriched vendor name (resolved from vendor_organization_id relationship)
|
||||
vendor_name: Optional[str] = None # Resolved from Organization.name via vendor_organization_id
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
242
backend/app/schemas/fleet_finance.py
Normal file
242
backend/app/schemas/fleet_finance.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/schemas/fleet_finance.py
|
||||
"""
|
||||
Pydantic sémák a flotta pénzügyi modelljeihez.
|
||||
- AssetFinancials: Beszerzés, finanszírozás, értékcsökkenés
|
||||
- InsuranceProvider: Biztosítók katalógusa
|
||||
- VehicleInsurancePolicy: Jármű biztosítási kötvények
|
||||
- VehicleTaxObligation: Jármű adókötelezettségek
|
||||
"""
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from typing import Optional, Dict, Any, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime, date
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AssetFinancials sémák
|
||||
# =============================================================================
|
||||
|
||||
class AssetFinancialsUpdate(BaseModel):
|
||||
"""AssetFinancials frissítése - minden mező opcionális."""
|
||||
purchase_price_net: Optional[float] = Field(None, ge=0, description="Nettó vételár")
|
||||
purchase_price_gross: Optional[float] = Field(None, ge=0, description="Bruttó vételár")
|
||||
vat_rate: Optional[float] = Field(None, ge=0, le=100, description="ÁFA kulcs (%)")
|
||||
activation_date: Optional[datetime] = Field(None, description="Aktiválás dátuma")
|
||||
verified_purchase_date: Optional[datetime] = Field(None, description="Ellenőrzött vásárlási dátum")
|
||||
financing_type: Optional[str] = Field(None, max_length=50, description="Finanszírozás típusa (cash, loan, lease)")
|
||||
accounting_details: Optional[Dict[str, Any]] = Field(None, description="Számviteli részletek (JSONB)")
|
||||
|
||||
# Finanszírozás
|
||||
down_payment: Optional[float] = Field(None, ge=0, description="Önerő")
|
||||
monthly_installment: Optional[float] = Field(None, ge=0, description="Havi törlesztő")
|
||||
residual_value: Optional[float] = Field(None, ge=0, description="Maradványérték")
|
||||
contract_number: Optional[str] = Field(None, max_length=100, description="Szerződésszám")
|
||||
financing_provider: Optional[str] = Field(None, max_length=200, description="Finanszírozó")
|
||||
interest_rate: Optional[float] = Field(None, ge=0, le=100, description="Kamatláb (%)")
|
||||
total_contract_value: Optional[float] = Field(None, ge=0, description="Teljes szerződéses érték")
|
||||
lease_start_date: Optional[date] = Field(None, description="Lízing kezdete")
|
||||
lease_end_date: Optional[date] = Field(None, description="Lízing vége")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class AssetFinancialsResponse(BaseModel):
|
||||
"""AssetFinancials teljes válaszmodell."""
|
||||
id: int
|
||||
asset_id: UUID
|
||||
|
||||
# Beszerzés
|
||||
purchase_price_net: float
|
||||
purchase_price_gross: float
|
||||
vat_rate: float
|
||||
activation_date: Optional[datetime] = None
|
||||
verified_purchase_date: Optional[datetime] = None
|
||||
financing_type: str
|
||||
accounting_details: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
# Finanszírozás
|
||||
down_payment: Optional[float] = None
|
||||
monthly_installment: Optional[float] = None
|
||||
residual_value: Optional[float] = None
|
||||
contract_number: Optional[str] = None
|
||||
financing_provider: Optional[str] = None
|
||||
interest_rate: Optional[float] = None
|
||||
total_contract_value: Optional[float] = None
|
||||
lease_start_date: Optional[date] = None
|
||||
lease_end_date: Optional[date] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# InsuranceProvider sémák
|
||||
# =============================================================================
|
||||
|
||||
class InsuranceProviderResponse(BaseModel):
|
||||
"""Biztosító teljes válaszmodell."""
|
||||
id: int
|
||||
name: str
|
||||
country_code: Optional[str] = Field(None, max_length=2, description="ISO 3166-1 alpha-2 országkód (pl. 'HU'). Ha None, akkor globális.")
|
||||
claim_phone: Optional[str] = None
|
||||
claim_url: Optional[str] = None
|
||||
services_offered: Dict[str, Any] = Field(default_factory=dict)
|
||||
is_active: bool = True
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class InsuranceProviderCreate(BaseModel):
|
||||
"""Új biztosító létrehozása."""
|
||||
name: str = Field(..., min_length=1, max_length=150, description="Biztosító neve")
|
||||
country_code: Optional[str] = Field(None, max_length=2, description="ISO 3166-1 alpha-2 országkód (pl. 'HU'). Ha None, akkor globális.")
|
||||
claim_phone: Optional[str] = Field(None, max_length=50, description="Kárbejelentő telefonszám")
|
||||
claim_url: Optional[str] = Field(None, max_length=255, description="Kárbejelentő URL")
|
||||
services_offered: Optional[Dict[str, Any]] = Field(None, description="Kínált szolgáltatások (JSONB)")
|
||||
is_active: bool = Field(default=True, description="Aktív-e")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class InsuranceProviderUpdate(BaseModel):
|
||||
"""Biztosító frissítése - minden mező opcionális."""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=150, description="Biztosító neve")
|
||||
country_code: Optional[str] = Field(None, max_length=2, description="ISO 3166-1 alpha-2 országkód (pl. 'HU'). Ha None, akkor globális.")
|
||||
claim_phone: Optional[str] = Field(None, max_length=50, description="Kárbejelentő telefonszám")
|
||||
claim_url: Optional[str] = Field(None, max_length=255, description="Kárbejelentő URL")
|
||||
services_offered: Optional[Dict[str, Any]] = Field(None, description="Kínált szolgáltatások (JSONB)")
|
||||
is_active: Optional[bool] = Field(None, description="Aktív-e")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VehicleInsurancePolicy sémák
|
||||
# =============================================================================
|
||||
|
||||
class VehicleInsurancePolicyCreate(BaseModel):
|
||||
"""Új biztosítási kötvény létrehozása."""
|
||||
provider_id: int = Field(..., description="Biztosító ID (insurance_providers tábla)")
|
||||
insurance_type: str = Field(..., max_length=20, description="Biztosítás típusa (KGFB, CASCO)")
|
||||
policy_number: str = Field(..., max_length=100, description="Kötvényszám")
|
||||
start_date: date = Field(..., description="Biztosítás kezdete")
|
||||
expiry_date: date = Field(..., description="Biztosítás lejárata")
|
||||
premium_amount: float = Field(..., ge=0, description="Díj összege")
|
||||
currency: str = Field(default="HUF", max_length=3, description="Pénznem")
|
||||
document_id: Optional[UUID] = Field(None, description="Kapcsolódó dokumentum ID (PDF kötvény)")
|
||||
|
||||
@field_validator('insurance_type')
|
||||
@classmethod
|
||||
def validate_insurance_type(cls, v: str) -> str:
|
||||
"""insurance_type csak KGFB vagy CASCO lehet."""
|
||||
allowed = {"KGFB", "CASCO"}
|
||||
cleaned = v.strip().upper()
|
||||
if cleaned not in allowed:
|
||||
raise ValueError(f'A biztosítás típusa csak {allowed} lehet, kapott: "{v}"')
|
||||
return cleaned
|
||||
|
||||
@field_validator('expiry_date')
|
||||
@classmethod
|
||||
def validate_expiry_after_start(cls, v: date, info):
|
||||
"""expiry_date nem lehet a start_date előtt."""
|
||||
values = info.data
|
||||
start = values.get('start_date')
|
||||
if start and v <= start:
|
||||
raise ValueError('A biztosítás lejárati dátuma nem lehet a kezdő dátum előtt vagy azzal egyenlő')
|
||||
return v
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class VehicleInsurancePolicyUpdate(BaseModel):
|
||||
"""Biztosítási kötvény frissítése - minden mező opcionális."""
|
||||
provider_id: Optional[int] = Field(None, description="Biztosító ID")
|
||||
insurance_type: Optional[str] = Field(None, max_length=20, description="Biztosítás típusa")
|
||||
policy_number: Optional[str] = Field(None, max_length=100, description="Kötvényszám")
|
||||
start_date: Optional[date] = Field(None, description="Biztosítás kezdete")
|
||||
expiry_date: Optional[date] = Field(None, description="Biztosítás lejárata")
|
||||
premium_amount: Optional[float] = Field(None, ge=0, description="Díj összege")
|
||||
currency: Optional[str] = Field(None, max_length=3, description="Pénznem")
|
||||
document_id: Optional[UUID] = Field(None, description="Kapcsolódó dokumentum ID (PDF kötvény)")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class VehicleInsurancePolicyResponse(BaseModel):
|
||||
"""Biztosítási kötvény teljes válaszmodell."""
|
||||
id: UUID
|
||||
asset_id: UUID
|
||||
provider_id: int
|
||||
insurance_type: str
|
||||
policy_number: str
|
||||
start_date: date
|
||||
expiry_date: date
|
||||
premium_amount: float
|
||||
currency: str
|
||||
document_id: Optional[UUID] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VehicleTaxObligation sémák
|
||||
# =============================================================================
|
||||
|
||||
class VehicleTaxObligationCreate(BaseModel):
|
||||
"""Új adókötelezettség létrehozása."""
|
||||
tax_type: str = Field(..., max_length=30, description="Adó típusa (WEIGHT_TAX, COMPANY_CAR_TAX)")
|
||||
tax_year: int = Field(..., ge=2000, le=2100, description="Adóév")
|
||||
amount: float = Field(..., ge=0, description="Adó összege")
|
||||
currency: str = Field(default="HUF", max_length=3, description="Pénznem")
|
||||
due_date: Optional[date] = Field(None, description="Fizetési határidő")
|
||||
payment_status: str = Field(default="pending", max_length=20, description="Fizetési státusz")
|
||||
document_id: Optional[UUID] = Field(None, description="Kapcsolódó dokumentum ID (PDF adóbevallás)")
|
||||
|
||||
@field_validator('tax_type')
|
||||
@classmethod
|
||||
def validate_tax_type(cls, v: str) -> str:
|
||||
"""tax_type csak WEIGHT_TAX vagy COMPANY_CAR_TAX lehet."""
|
||||
allowed = {"WEIGHT_TAX", "COMPANY_CAR_TAX"}
|
||||
cleaned = v.strip().upper()
|
||||
if cleaned not in allowed:
|
||||
raise ValueError(f'Az adó típusa csak {allowed} lehet, kapott: "{v}"')
|
||||
return cleaned
|
||||
|
||||
@field_validator('payment_status')
|
||||
@classmethod
|
||||
def validate_payment_status(cls, v: str) -> str:
|
||||
"""payment_status csak pending, paid, overdue lehet."""
|
||||
allowed = {"pending", "paid", "overdue"}
|
||||
cleaned = v.strip().lower()
|
||||
if cleaned not in allowed:
|
||||
raise ValueError(f'A fizetési státusz csak {allowed} lehet, kapott: "{v}"')
|
||||
return cleaned
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class VehicleTaxObligationUpdate(BaseModel):
|
||||
"""Adókötelezettség frissítése - minden mező opcionális."""
|
||||
tax_type: Optional[str] = Field(None, max_length=30, description="Adó típusa")
|
||||
tax_year: Optional[int] = Field(None, ge=2000, le=2100, description="Adóév")
|
||||
amount: Optional[float] = Field(None, ge=0, description="Adó összege")
|
||||
currency: Optional[str] = Field(None, max_length=3, description="Pénznem")
|
||||
due_date: Optional[date] = Field(None, description="Fizetési határidő")
|
||||
payment_status: Optional[str] = Field(None, max_length=20, description="Fizetési státusz")
|
||||
document_id: Optional[UUID] = Field(None, description="Kapcsolódó dokumentum ID (PDF adóbevallás)")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class VehicleTaxObligationResponse(BaseModel):
|
||||
"""Adókötelezettség teljes válaszmodell."""
|
||||
id: UUID
|
||||
asset_id: UUID
|
||||
tax_type: str
|
||||
tax_year: int
|
||||
amount: float
|
||||
currency: str
|
||||
due_date: Optional[date] = None
|
||||
payment_status: str
|
||||
document_id: Optional[UUID] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -1,5 +1,6 @@
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from typing import Optional, List, Any
|
||||
from datetime import datetime
|
||||
|
||||
class ContactCreate(BaseModel):
|
||||
full_name: str
|
||||
@@ -78,7 +79,11 @@ class OrganizationResponse(BaseModel):
|
||||
is_active: bool = True
|
||||
is_deleted: bool = False
|
||||
subscription_plan: str = "FREE"
|
||||
subscription_expires_at: Optional[datetime] = None
|
||||
subscription_tier_id: Optional[int] = None
|
||||
# P0: Real subscription limits from the assigned subscription_tier JSONB rules
|
||||
max_vehicles: int = 1
|
||||
max_garages: int = 1
|
||||
visual_settings: Optional[dict] = None
|
||||
notification_settings: Optional[Any] = None
|
||||
external_integration_config: Optional[Any] = None
|
||||
|
||||
@@ -56,6 +56,10 @@ class UserResponse(UserBase):
|
||||
person_id: Optional[int] = None
|
||||
role: str
|
||||
subscription_plan: str
|
||||
subscription_expires_at: Optional[datetime] = None
|
||||
# P0: Real subscription limits from the assigned subscription_tier JSONB rules
|
||||
max_vehicles: int = 1
|
||||
max_garages: int = 1
|
||||
scope_level: str
|
||||
scope_id: Optional[str] = None
|
||||
ui_mode: str = "personal"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/services/analytics_service.py
|
||||
"""
|
||||
TCO (Total Cost of Ownership) Analytics Service.
|
||||
Számítások a vehicle.costs tábla alapján, árfolyam-átváltással a system_service segítségével.
|
||||
Számítások a fleet_finance.asset_costs tábla alapján, árfolyam-átváltással a system_service segítségével.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -10,7 +10,7 @@ from sqlalchemy import select, func, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.vehicle import VehicleCost, CostCategory
|
||||
from app.models.fleet_finance import AssetCost, CostCategory
|
||||
from app.models import VehicleModelDefinition
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.services.system_service import SystemService
|
||||
@@ -56,22 +56,22 @@ class TCOAnalytics:
|
||||
"""
|
||||
# Alap lekérdezés: organization_id szűrés
|
||||
stmt = select(
|
||||
VehicleCost.amount,
|
||||
VehicleCost.currency,
|
||||
VehicleCost.category_id,
|
||||
AssetCost.amount_net,
|
||||
AssetCost.currency,
|
||||
AssetCost.category_id,
|
||||
CostCategory.code,
|
||||
CostCategory.name
|
||||
).join(
|
||||
CostCategory, VehicleCost.category_id == CostCategory.id
|
||||
CostCategory, AssetCost.category_id == CostCategory.id
|
||||
).where(
|
||||
VehicleCost.organization_id == organization_id
|
||||
AssetCost.organization_id == organization_id
|
||||
)
|
||||
|
||||
# Dátum szűrés
|
||||
if start_date:
|
||||
stmt = stmt.where(VehicleCost.date >= start_date)
|
||||
stmt = stmt.where(AssetCost.date >= start_date)
|
||||
if end_date:
|
||||
stmt = stmt.where(VehicleCost.date <= end_date)
|
||||
stmt = stmt.where(AssetCost.date <= end_date)
|
||||
|
||||
# Kategória szűrés
|
||||
if include_categories:
|
||||
@@ -87,7 +87,7 @@ class TCOAnalytics:
|
||||
category_totals = {}
|
||||
|
||||
for row in rows:
|
||||
amount = float(row.amount)
|
||||
amount = float(row.amount_net)
|
||||
source_currency = row.currency
|
||||
|
||||
# Átváltás célvalutára
|
||||
@@ -145,14 +145,14 @@ class TCOAnalytics:
|
||||
"""
|
||||
# Összes költség lekérdezése a járműhöz
|
||||
stmt = select(
|
||||
VehicleCost.amount,
|
||||
VehicleCost.currency,
|
||||
VehicleCost.organization_id,
|
||||
AssetCost.amount_net,
|
||||
AssetCost.currency,
|
||||
AssetCost.organization_id,
|
||||
Organization.name.label("org_name")
|
||||
).outerjoin(
|
||||
Organization, VehicleCost.organization_id == Organization.id
|
||||
Organization, AssetCost.organization_id == Organization.id
|
||||
).where(
|
||||
VehicleCost.vehicle_id == vehicle_model_id
|
||||
AssetCost.asset_id == vehicle_model_id
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
@@ -166,7 +166,7 @@ class TCOAnalytics:
|
||||
owner_totals = {}
|
||||
|
||||
for row in rows:
|
||||
amount = float(row.amount)
|
||||
amount = float(row.amount_net)
|
||||
source_currency = row.currency
|
||||
|
||||
# Átváltás célvalutára
|
||||
@@ -238,23 +238,22 @@ class TCOAnalytics:
|
||||
"""
|
||||
# Alap lekérdezés: vehicle és cost összekapcsolása
|
||||
stmt = select(
|
||||
VehicleCost.amount,
|
||||
VehicleCost.currency,
|
||||
VehicleCost.vehicle_id,
|
||||
VehicleCost.odometer,
|
||||
AssetCost.amount_net,
|
||||
AssetCost.currency,
|
||||
AssetCost.asset_id,
|
||||
CostCategory.code,
|
||||
VehicleModelDefinition.make,
|
||||
VehicleModelDefinition.model,
|
||||
VehicleModelDefinition.fuel_type
|
||||
).join(
|
||||
VehicleModelDefinition, VehicleCost.vehicle_id == VehicleModelDefinition.id
|
||||
VehicleModelDefinition, AssetCost.asset_id == VehicleModelDefinition.id
|
||||
).join(
|
||||
CostCategory, VehicleCost.category_id == CostCategory.id
|
||||
CostCategory, AssetCost.category_id == CostCategory.id
|
||||
)
|
||||
|
||||
# Szűrés
|
||||
if vehicle_model_id:
|
||||
stmt = stmt.where(VehicleCost.vehicle_id == vehicle_model_id)
|
||||
stmt = stmt.where(AssetCost.asset_id == vehicle_model_id)
|
||||
benchmark_type = "specific_model"
|
||||
else:
|
||||
conditions = []
|
||||
@@ -295,7 +294,7 @@ class TCOAnalytics:
|
||||
category_counts = {}
|
||||
|
||||
for row in rows:
|
||||
amount = float(row.amount)
|
||||
amount = float(row.amount_net)
|
||||
source_currency = row.currency
|
||||
|
||||
# Átváltás
|
||||
@@ -304,11 +303,7 @@ class TCOAnalytics:
|
||||
)
|
||||
|
||||
total_cost_sum += converted_amount
|
||||
vehicle_ids.add(row.vehicle_id)
|
||||
|
||||
# Odometer összegzés (ha van)
|
||||
if row.odometer:
|
||||
total_odometer_sum += row.odometer
|
||||
vehicle_ids.add(row.asset_id)
|
||||
|
||||
# Kategória összesítés
|
||||
category_code = row.code
|
||||
@@ -322,11 +317,6 @@ class TCOAnalytics:
|
||||
vehicle_count = len(vehicle_ids)
|
||||
average_cost_per_vehicle = round(total_cost_sum / vehicle_count, 2)
|
||||
|
||||
# Kilométerenkénti átlag számítása
|
||||
average_cost_per_km = None
|
||||
if total_odometer_sum > 0:
|
||||
average_cost_per_km = round(total_cost_sum / total_odometer_sum, 4)
|
||||
|
||||
# Kategóriánkénti átlagok
|
||||
category_averages = {}
|
||||
for code, total in category_totals.items():
|
||||
@@ -342,7 +332,7 @@ class TCOAnalytics:
|
||||
"vehicle_count": vehicle_count,
|
||||
"total_cost_sum": round(total_cost_sum, 2),
|
||||
"average_cost_per_vehicle": average_cost_per_vehicle,
|
||||
"average_cost_per_km": average_cost_per_km,
|
||||
"average_cost_per_km": None,
|
||||
"by_category": category_averages,
|
||||
"currency": currency_target,
|
||||
"criteria": {
|
||||
|
||||
@@ -216,6 +216,19 @@ class AssetService:
|
||||
# Timeline
|
||||
'year_of_manufacture': asset_data.year_of_manufacture,
|
||||
'first_registration_date': asset_data.first_registration_date,
|
||||
|
||||
# Registration Documents
|
||||
'registration_certificate_number': asset_data.registration_certificate_number,
|
||||
'registration_certificate_validity': asset_data.registration_certificate_validity,
|
||||
'vehicle_registration_document_number': asset_data.vehicle_registration_document_number,
|
||||
|
||||
# Internationalization (ÚJ)
|
||||
'registration_country': asset_data.registration_country,
|
||||
'first_domestic_registration_date': asset_data.first_domestic_registration_date,
|
||||
'import_country': asset_data.import_country,
|
||||
'title_document_number': asset_data.title_document_number,
|
||||
'engine_number': asset_data.engine_number,
|
||||
'number_of_previous_owners': asset_data.number_of_previous_owners,
|
||||
}
|
||||
|
||||
# Remove None values from the dictionary
|
||||
|
||||
@@ -16,7 +16,7 @@ import logging
|
||||
from typing import Optional, List
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from app.models.vehicle.vehicle import CostCategory
|
||||
from app.models.fleet_finance import CostCategory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
245
backend/scripts/seed_finance_dictionaries.py
Normal file
245
backend/scripts/seed_finance_dictionaries.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Seed script: fleet_finance dictionary tables (CostCategory & InsuranceProvider).
|
||||
|
||||
Idempotent seeder that populates:
|
||||
1. fleet_finance.cost_categories - Hierarchical cost categories (5 parents + children)
|
||||
2. fleet_finance.insurance_providers - Base insurance providers (HU/EU)
|
||||
|
||||
Usage:
|
||||
docker compose exec sf_api python -m backend.scripts.seed_finance_dictionaries
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select, text
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.fleet_finance.models import CostCategory, InsuranceProvider
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =============================================================================
|
||||
# Cost Categories - Hierarchical structure
|
||||
# =============================================================================
|
||||
# Format: (code, name, description, children)
|
||||
# Children format: (code, name, description)
|
||||
|
||||
COST_CATEGORY_TREE = [
|
||||
{
|
||||
"code": "FINANCING",
|
||||
"name": "Finanszírozás",
|
||||
"description": "Jármű finanszírozásával kapcsolatos költségek",
|
||||
"children": [
|
||||
("FINANCING_LEASE", "Lízingdíj", "Lízingdíj fizetés"),
|
||||
("FINANCING_LOAN", "Hiteltörlesztő", "Hiteltörlesztő részlet"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"code": "INSURANCE",
|
||||
"name": "Biztosítás",
|
||||
"description": "Biztosítási díjak és kötvények",
|
||||
"children": [
|
||||
("INSURANCE_KGFB", "KGFB", "Kötelező gépjármű-felelősségbiztosítás"),
|
||||
("INSURANCE_CASCO", "Casco", "Casco biztosítás"),
|
||||
("INSURANCE_ASSISTANCE", "Assistance", "Assistance szolgáltatás"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"code": "ADMIN_FEES",
|
||||
"name": "Hatósági díjak",
|
||||
"description": "Hatósági és adminisztratív költségek",
|
||||
"children": [
|
||||
("ADMIN_FEES_VEHICLE_TAX", "Gépjárműadó", "Gépjárműadó fizetés"),
|
||||
("ADMIN_FEES_MOT", "Műszaki vizsga", "Műszaki vizsgadíj"),
|
||||
("ADMIN_FEES_DOCS", "Okmányok", "Okmányokkal kapcsolatos díjak"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"code": "OPERATION",
|
||||
"name": "Üzemeltetés",
|
||||
"description": "Jármű üzemeltetésével kapcsolatos költségek",
|
||||
"children": [
|
||||
("OPERATION_FUEL", "Üzemanyag", "Üzemanyag költség"),
|
||||
("OPERATION_ELECTRIC", "Elektromos töltés", "Elektromos töltési költség"),
|
||||
("OPERATION_TOLL", "Autópálya", "Autópálya használati díj"),
|
||||
("OPERATION_PARKING", "Parkolás", "Parkolási díj"),
|
||||
("OPERATION_WASH", "Mosás", "Járműmosás és ápolás"),
|
||||
],
|
||||
},
|
||||
{
|
||||
"code": "SERVICE",
|
||||
"name": "Szerviz",
|
||||
"description": "Szerviz és karbantartási költségek",
|
||||
"children": [
|
||||
("SERVICE_SCHEDULED", "Kötelező szerviz", "Kötelező időszakos szerviz"),
|
||||
("SERVICE_REPAIR", "Eseti javítás", "Eseti javítási költség"),
|
||||
("SERVICE_TIRES", "Gumiabroncs", "Gumiabroncs csere és tárolás"),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# =============================================================================
|
||||
# Insurance Providers
|
||||
# =============================================================================
|
||||
|
||||
INSURANCE_PROVIDERS = [
|
||||
{"name": "Allianz Hungária Zrt.", "claim_phone": "+36-1-301-7100", "claim_url": "https://www.allianz.hu/karterites"},
|
||||
{"name": "Generali Biztosító Zrt.", "claim_phone": "+36-1-452-3200", "claim_url": "https://www.generali.hu/karterites"},
|
||||
{"name": "K&H Biztosító Zrt.", "claim_phone": "+36-1-335-3300", "claim_url": "https://www.kh.hu/biztositas/karterites"},
|
||||
{"name": "Groupama Biztosító Zrt.", "claim_phone": "+36-1-477-4800", "claim_url": "https://www.groupama.hu/karterites"},
|
||||
{"name": "Uniqa Biztosító Zrt.", "claim_phone": "+36-1-301-7300", "claim_url": "https://www.uniqa.hu/karterites"},
|
||||
{"name": "Aegon Magyarország / Alfa Vienna Insurance Group", "claim_phone": "+36-1-477-4700", "claim_url": "https://www.alfabiztosito.hu/karterites"},
|
||||
{"name": "Signal Biztosító Zrt.", "claim_phone": "+36-1-457-4400", "claim_url": "https://www.signal.hu/karterites"},
|
||||
{"name": "Wáberer Hungária Biztosító Zrt.", "claim_phone": "+36-1-237-7000", "claim_url": "https://www.whb.hu/karterites"},
|
||||
{"name": "Posta Biztosító Zrt.", "claim_phone": "+36-1-477-4900", "claim_url": "https://www.postabiztosito.hu/karterites"},
|
||||
{"name": "Magyar Posta Életbiztosító Zrt.", "claim_phone": "+36-1-477-5000", "claim_url": "https://www.mpelb.hu/karterites"},
|
||||
]
|
||||
|
||||
|
||||
async def seed_cost_categories(db: AsyncSession) -> int:
|
||||
"""Seed cost_categories table with hierarchical data. Returns count of inserted parents."""
|
||||
inserted_count = 0
|
||||
|
||||
for parent_def in COST_CATEGORY_TREE:
|
||||
# Check if parent already exists by code
|
||||
existing_parent = await db.execute(
|
||||
select(CostCategory).where(CostCategory.code == parent_def["code"])
|
||||
)
|
||||
parent = existing_parent.scalar_one_or_none()
|
||||
|
||||
if not parent:
|
||||
parent = CostCategory(
|
||||
code=parent_def["code"],
|
||||
name=parent_def["name"],
|
||||
description=parent_def["description"],
|
||||
is_system=True,
|
||||
visibility="both",
|
||||
min_tier="free",
|
||||
parent_id=None,
|
||||
)
|
||||
db.add(parent)
|
||||
await db.flush() # Get the parent ID
|
||||
inserted_count += 1
|
||||
logger.info(f" ✅ PARENT [{parent.code:20s}] {parent.name}")
|
||||
|
||||
# Process children
|
||||
for child_code, child_name, child_desc in parent_def["children"]:
|
||||
existing_child = await db.execute(
|
||||
select(CostCategory).where(CostCategory.code == child_code)
|
||||
)
|
||||
child = existing_child.scalar_one_or_none()
|
||||
|
||||
if not child:
|
||||
child = CostCategory(
|
||||
code=child_code,
|
||||
name=child_name,
|
||||
description=child_desc,
|
||||
is_system=True,
|
||||
visibility="both",
|
||||
min_tier="free",
|
||||
parent_id=parent.id,
|
||||
)
|
||||
db.add(child)
|
||||
await db.flush()
|
||||
logger.info(f" └─ CHILD [{child.code:20s}] {child.name}")
|
||||
else:
|
||||
logger.info(f" └─ EXISTS [{child.code:20s}] {child.name}")
|
||||
|
||||
return inserted_count
|
||||
|
||||
|
||||
async def seed_insurance_providers(db: AsyncSession) -> int:
|
||||
"""Seed insurance_providers table. Returns count of inserted providers."""
|
||||
inserted_count = 0
|
||||
|
||||
for prov_def in INSURANCE_PROVIDERS:
|
||||
existing = await db.execute(
|
||||
select(InsuranceProvider).where(InsuranceProvider.name == prov_def["name"])
|
||||
)
|
||||
provider = existing.scalar_one_or_none()
|
||||
|
||||
if not provider:
|
||||
provider = InsuranceProvider(
|
||||
name=prov_def["name"],
|
||||
country_code="HU",
|
||||
claim_phone=prov_def["claim_phone"],
|
||||
claim_url=prov_def["claim_url"],
|
||||
services_offered={"kgfb": True, "casco": True},
|
||||
is_active=True,
|
||||
)
|
||||
db.add(provider)
|
||||
inserted_count += 1
|
||||
logger.info(f" ✅ {provider.name} (HU)")
|
||||
else:
|
||||
# Update existing provider's country_code if not set
|
||||
if not provider.country_code:
|
||||
provider.country_code = "HU"
|
||||
logger.info(f" UPDATED {provider.name} → country_code='HU'")
|
||||
else:
|
||||
logger.info(f" EXISTS {provider.name} (country_code='{provider.country_code}')")
|
||||
|
||||
return inserted_count
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main seeder entry point."""
|
||||
engine = create_async_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
async with async_session() as db:
|
||||
try:
|
||||
logger.info("=" * 60)
|
||||
logger.info("📦 fleet_finance dictionary seeder")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# --- Cost Categories ---
|
||||
logger.info("\n📁 Cost Categories (hierarchical):")
|
||||
cat_count = await seed_cost_categories(db)
|
||||
await db.commit()
|
||||
logger.info(f" → {cat_count} new parent categories inserted")
|
||||
|
||||
# --- Insurance Providers ---
|
||||
logger.info("\n🏢 Insurance Providers:")
|
||||
prov_count = await seed_insurance_providers(db)
|
||||
await db.commit()
|
||||
logger.info(f" → {prov_count} new providers inserted")
|
||||
|
||||
# --- Summary ---
|
||||
total_cats = await db.execute(
|
||||
select(CostCategory).where(CostCategory.parent_id.is_(None))
|
||||
)
|
||||
parents = total_cats.scalars().all()
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("📊 SEED SUMMARY")
|
||||
logger.info("=" * 60)
|
||||
logger.info(f" CostCategory parents: {len(parents)}")
|
||||
for p in parents:
|
||||
child_count = await db.execute(
|
||||
select(CostCategory).where(CostCategory.parent_id == p.id)
|
||||
)
|
||||
children = child_count.scalars().all()
|
||||
logger.info(f" ├─ {p.code:20s} ({p.name}) → {len(children)} children")
|
||||
|
||||
total_providers = await db.execute(
|
||||
select(InsuranceProvider).where(InsuranceProvider.is_active.is_(True))
|
||||
)
|
||||
providers = total_providers.scalars().all()
|
||||
logger.info(f" InsuranceProviders (active): {len(providers)}")
|
||||
for p in providers:
|
||||
logger.info(f" ├─ {p.name}")
|
||||
|
||||
logger.info("\n✅ Seeding completed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error(f"❌ Seeding failed: {e}")
|
||||
raise
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -4,14 +4,21 @@
|
||||
"SUCCESS": "Login successful. Welcome back!"
|
||||
},
|
||||
"ERROR": {
|
||||
"EMAIL_EXISTS": "This email is already registered."
|
||||
"EMAIL_EXISTS": "This email is already registered.",
|
||||
"UNAUTHORIZED": "Unauthorized access."
|
||||
}
|
||||
},
|
||||
"SENTINEL": {
|
||||
"LOCK": {
|
||||
"MSG": "Account locked for security reasons."
|
||||
},
|
||||
"APPROVAL": {
|
||||
"REQUIRED": "Approval required"
|
||||
}
|
||||
},
|
||||
"COMMON": {
|
||||
"SAVE_SUCCESS": "Saved successfully!"
|
||||
},
|
||||
"EMAIL": {
|
||||
"REG": {
|
||||
"SUBJECT": "Confirm your registration - Service Finder",
|
||||
@@ -72,5 +79,11 @@
|
||||
"OTP_SENT": "A 6-digit verification code has been sent to the organization owner's email.",
|
||||
"SUCCESS": "You have successfully taken over the organization. You are now the admin."
|
||||
}
|
||||
},
|
||||
"REGISTRATION_DOCUMENTS": {
|
||||
"CERTIFICATE_NUMBER": "Registration Certificate Number",
|
||||
"CERTIFICATE_VALIDITY": "Registration Certificate Validity",
|
||||
"DOCUMENT_NUMBER": "Vehicle Registration Document Number",
|
||||
"TITLE": "Registration Certificate & Vehicle Document"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"ERROR": {
|
||||
"EMAIL_EXISTS": "Ez az e-mail cím már foglalt.",
|
||||
"UNAUTHORIZED": "Nincs jogosultságod a művelethez."
|
||||
"UNAUTHORIZED": "Jogosulatlan hozzáférés"
|
||||
}
|
||||
},
|
||||
"SENTINEL": {
|
||||
@@ -79,5 +79,11 @@
|
||||
"OTP_SENT": "Egy 6-jegyű megerősítő kódot küldtünk a cég tulajdonosának email címére.",
|
||||
"SUCCESS": "Sikeresen átvetted a szervezet irányítását. Mostantól te vagy az admin."
|
||||
}
|
||||
},
|
||||
"REGISTRATION_DOCUMENTS": {
|
||||
"CERTIFICATE_NUMBER": "Forgalmi engedély száma",
|
||||
"CERTIFICATE_VALIDITY": "Forgalmi engedély érvényessége",
|
||||
"DOCUMENT_NUMBER": "Törzskönyv száma",
|
||||
"TITLE": "Forgalmi engedély és Törzskönyv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user