Initial commit: Robot ökoszisztéma v2.0 - Stabilizált jármű és szerviz robotok
This commit is contained in:
144
backend/app/services/cost_service.py
Executable file
144
backend/app/services/cost_service.py
Executable file
@@ -0,0 +1,144 @@
|
||||
# /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.asset 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()
|
||||
Reference in New Issue
Block a user