144 lines
5.9 KiB
Python
Executable File
144 lines
5.9 KiB
Python
Executable File
# /opt/docker/dev/service_finder/backend/app/services/cost_service.py
|
|
import uuid
|
|
import logging
|
|
from decimal import Decimal
|
|
from typing import Any, Dict
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, desc, func
|
|
from app.models import AssetCost, AssetTelemetry, ExchangeRate
|
|
from app.services.gamification_service import GamificationService
|
|
from app.services.config_service import config
|
|
from app.schemas.asset_cost import AssetCostCreate
|
|
from datetime import datetime
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class CostService:
|
|
"""
|
|
Industrial Cost & Telemetry Service.
|
|
Összeköti a pénzügyi kiadásokat, az OCR bizonylatokat és a jármű állapotát.
|
|
"""
|
|
|
|
async def record_cost(self, db: AsyncSession, cost_in: AssetCostCreate, user_id: int):
|
|
""" Teljes körű költségrögzítés: Konverzió + Telemetria + OCR + XP. """
|
|
try:
|
|
# 1. Dinamikus konfiguráció lekérése
|
|
base_currency = await config.get_setting(db, "finance_base_currency", default="EUR")
|
|
base_xp = await config.get_setting(db, "xp_per_cost_log", default=50)
|
|
ocr_multiplier = await config.get_setting(db, "xp_multiplier_ocr_cost", default=1.5)
|
|
|
|
# 2. Intelligens Árfolyamkezelés
|
|
exchange_rate = Decimal("1.0")
|
|
if cost_in.currency_local != base_currency:
|
|
rate_stmt = select(ExchangeRate).where(
|
|
ExchangeRate.target_currency == cost_in.currency_local
|
|
).order_by(desc(ExchangeRate.updated_at)).limit(1)
|
|
rate_res = await db.execute(rate_stmt)
|
|
rate_obj = rate_res.scalar_one_or_none()
|
|
exchange_rate = rate_obj.rate if rate_obj else Decimal("1.0")
|
|
|
|
amt_base = Decimal(str(cost_in.amount_local)) / exchange_rate if exchange_rate > 0 else Decimal("0")
|
|
|
|
# 3. Költség rekord rögzítése (Kapcsolva a Robot 1 OCR dokumentumához)
|
|
new_cost = AssetCost(
|
|
asset_id=cost_in.asset_id,
|
|
organization_id=cost_in.organization_id,
|
|
driver_id=user_id,
|
|
cost_type=cost_in.cost_type,
|
|
amount_local=cost_in.amount_local,
|
|
currency_local=cost_in.currency_local,
|
|
amount_eur=amt_base,
|
|
net_amount_local=cost_in.net_amount_local,
|
|
vat_rate=cost_in.vat_rate,
|
|
exchange_rate_used=exchange_rate,
|
|
mileage_at_cost=cost_in.mileage_at_cost,
|
|
date=cost_in.date or datetime.now(),
|
|
# OCR Kapcsolat
|
|
document_id=cost_in.document_id,
|
|
is_ai_generated=cost_in.document_id is not None,
|
|
data=cost_in.data or {}
|
|
)
|
|
db.add(new_cost)
|
|
|
|
# 4. Automatikus Telemetria (Kilométeróra frissítés)
|
|
if cost_in.mileage_at_cost:
|
|
await self._sync_telemetry(db, cost_in.asset_id, cost_in.mileage_at_cost)
|
|
|
|
# 5. Gamification (Értékesebb az adat, ha van róla fotó/OCR)
|
|
final_xp = base_xp
|
|
if new_cost.is_ai_generated:
|
|
final_xp = int(base_xp * float(ocr_multiplier))
|
|
|
|
await GamificationService.award_points(
|
|
db, user_id=user_id, amount=final_xp, reason=f"EXPENSE_LOG_{cost_in.cost_type}"
|
|
)
|
|
|
|
await db.commit()
|
|
await db.refresh(new_cost)
|
|
return new_cost
|
|
|
|
except Exception as e:
|
|
await db.rollback()
|
|
logger.error(f"CostService Error: {e}")
|
|
raise e
|
|
|
|
async def _sync_telemetry(self, db: AsyncSession, asset_id: int, mileage: int):
|
|
""" Segédfüggvény: Biztonságos óraállás frissítés. """
|
|
stmt = select(AssetTelemetry).where(AssetTelemetry.asset_id == asset_id)
|
|
res = await db.execute(stmt)
|
|
telemetry = res.scalar_one_or_none()
|
|
|
|
if telemetry:
|
|
# Csak akkor frissítünk, ha az új érték nagyobb (nincs visszatekerés)
|
|
if mileage > (telemetry.current_mileage or 0):
|
|
telemetry.current_mileage = mileage
|
|
telemetry.last_updated = datetime.now()
|
|
else:
|
|
db.add(AssetTelemetry(asset_id=asset_id, current_mileage=mileage))
|
|
|
|
async def get_asset_financial_summary(self, db: AsyncSession, asset_id: uuid.UUID) -> Dict[str, Any]:
|
|
"""
|
|
Dinamikus pénzügyi összesítő SQL szintű aggregációval.
|
|
MB 2.0: Nem loopolunk Pythonban, a DB számol!
|
|
"""
|
|
# 1. Lekérjük az összesített adatokat kategóriánként (Local és EUR)
|
|
stmt = (
|
|
select(
|
|
AssetCost.cost_type,
|
|
func.sum(AssetCost.amount_local).label("total_local"),
|
|
func.sum(AssetCost.amount_eur).label("total_eur"),
|
|
func.count(AssetCost.id).label("transaction_count")
|
|
)
|
|
.where(AssetCost.asset_id == asset_id)
|
|
.group_by(AssetCost.cost_type)
|
|
)
|
|
|
|
res = await db.execute(stmt)
|
|
rows = res.all()
|
|
|
|
summary = {
|
|
"by_category": {},
|
|
"grand_total_local": Decimal("0.0"),
|
|
"grand_total_eur": Decimal("0.0"),
|
|
"total_transactions": 0
|
|
}
|
|
|
|
for row in rows:
|
|
cat = row.cost_type or "OTHER"
|
|
summary["by_category"][cat] = {
|
|
"local": float(row.total_local),
|
|
"eur": float(row.total_eur),
|
|
"count": row.transaction_count
|
|
}
|
|
summary["grand_total_local"] += row.total_local
|
|
summary["grand_total_eur"] += row.total_eur
|
|
summary["total_transactions"] += row.transaction_count
|
|
|
|
# Decimal konverzió a JSON-höz
|
|
summary["grand_total_local"] = float(summary["grand_total_local"])
|
|
summary["grand_total_eur"] = float(summary["grand_total_eur"])
|
|
|
|
return summary
|
|
|
|
cost_service = CostService() |