2026.03.29 20:00 Gitea_manager javítás előtt

This commit is contained in:
Roo
2026-03-29 17:59:06 +00:00
parent 03258db091
commit ba8b6579ef
148 changed files with 7951 additions and 591 deletions

View File

@@ -5,10 +5,10 @@ import uuid
from typing import List, Optional, Dict, Any, TYPE_CHECKING
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from sqlalchemy import select, func, and_, distinct
from sqlalchemy.orm import selectinload
from app.models import Asset, AssetAssignment, AssetTelemetry, AssetFinancials
from app.models import Asset, AssetAssignment, AssetTelemetry, AssetFinancials, VehicleModelDefinition
from app.models.identity import User
from app.models.vehicle.history import LogSeverity
from app.services.config_service import config
@@ -52,9 +52,9 @@ class AssetService:
user_stmt = select(User).where(User.id == user_id)
user = (await db.execute(user_stmt)).scalar_one()
limits = await config.get_setting(db, "VEHICLE_LIMIT", default={"free": 1, "premium": 5, "vip": 50})
user_role = user.role.value if hasattr(user.role, 'value') else str(user.role)
allowed_limit = limits.get(user_role, 1)
# Get vehicle limit using the new function that checks both user AND organization limits
# Returns the HIGHER value of user-specific and organization-specific limits
allowed_limit = await AssetService.get_user_vehicle_limit(db, user_id, org_id)
# Csak aktív járművek számítanak a limitbe (draft-ok nem)
count_stmt = select(func.count(Asset.id)).where(
@@ -83,12 +83,23 @@ class AssetService:
)
# 3. ÚJ JÁRMŰ LÉTREHOZÁSA (Standard vagy Draft Flow)
status = "draft" if draft or not vin_clean else "active"
# Dynamic Gatekeeper Logic: If both vin and catalog_id are missing, status = 'draft'
# If core data is provided (either vin OR catalog_id), status = 'active'
# Also respect the draft parameter if explicitly set
if draft:
status = "draft"
elif not vin_clean and not catalog_id:
status = "draft"
else:
status = "active"
new_asset = Asset(
vin=vin_clean,
license_plate=license_plate_clean,
catalog_id=catalog_id,
current_organization_id=org_id,
owner_person_id=user.person_id,
owner_org_id=org_id,
status=status,
individual_equipment={},
created_at=datetime.utcnow()
@@ -109,6 +120,9 @@ class AssetService:
# Gamification
reward = await config.get_setting(db, "xp_reward_asset_register", default=250)
await GamificationService.award_points(db, user_id, int(reward), "NEW_ASSET_REG")
# Check if this is user's first vehicle and award "First Car" badge
await AssetService._award_first_car_badge(db, user_id, org_id)
await db.commit()
return new_asset
@@ -136,7 +150,7 @@ class AssetService:
if auto_transfer:
# Csak akkor, ha a régi tulajdonos 'sold' állapotba tette
if asset.status == "sold":
return await AssetService.execute_final_transfer(db, asset, org_id, new_plate)
return await AssetService.execute_final_transfer(db, asset, org_id, new_plate, user_id)
# Függőben lévő állapot: Dokumentum feltöltésre vár
asset.status = "transfer_pending"
@@ -150,7 +164,7 @@ class AssetService:
)
@staticmethod
async def execute_final_transfer(db: AsyncSession, asset: Asset, new_org_id: int, new_plate: str):
async def execute_final_transfer(db: AsyncSession, asset: Asset, new_org_id: int, new_plate: str, user_id: int = None):
""" A tulajdonjog tényleges átírása az adatbázisban. """
# 1. Régi hozzárendelés lezárása
await db.execute(
@@ -165,7 +179,193 @@ class AssetService:
asset.status = "active"
asset.is_verified = False # Az új tulajdonos papírjait is ellenőrizni kell!
# 3. Update ownership fields if user_id is provided
if user_id is not None:
from app.models.identity import User
user_stmt = select(User).where(User.id == user_id)
user = (await db.execute(user_stmt)).scalar_one_or_none()
if user and user.person_id:
asset.owner_person_id = user.person_id
asset.owner_org_id = new_org_id
else:
logger.warning(f"User {user_id} has no person_id, cannot set owner_person_id")
else:
logger.warning("execute_final_transfer called without user_id, ownership fields not updated")
db.add(AssetAssignment(asset_id=asset.id, organization_id=new_org_id, status="active"))
await db.commit()
return asset
return asset
# --- CATALOG METHODS ---
@staticmethod
async def get_makes(db: AsyncSession) -> List[str]:
"""Get all distinct makes from vehicle model definitions."""
stmt = select(distinct(VehicleModelDefinition.make)).order_by(VehicleModelDefinition.make)
result = await db.execute(stmt)
makes = result.scalars().all()
return [make for make in makes if make] # Filter out None/empty
@staticmethod
async def get_models(db: AsyncSession, make: str) -> List[str]:
"""Get all distinct models for a given make."""
stmt = select(distinct(VehicleModelDefinition.marketing_name)).where(
VehicleModelDefinition.make == make
).order_by(VehicleModelDefinition.marketing_name)
result = await db.execute(stmt)
models = result.scalars().all()
return [model for model in models if model]
@staticmethod
async def get_generations(db: AsyncSession, make: str, model: str) -> List[str]:
"""Get all distinct generations/variants for a given make and model.
For now, we'll use engine_code as generation placeholder."""
stmt = select(distinct(VehicleModelDefinition.engine_code)).where(
VehicleModelDefinition.make == make,
VehicleModelDefinition.marketing_name == model,
VehicleModelDefinition.engine_code.isnot(None)
).order_by(VehicleModelDefinition.engine_code)
result = await db.execute(stmt)
generations = result.scalars().all()
return [gen for gen in generations if gen]
@staticmethod
async def get_engines(db: AsyncSession, make: str, model: str, gen: str) -> List[VehicleModelDefinition]:
"""Get all engine variants for a given make, model, and generation."""
stmt = select(VehicleModelDefinition).where(
VehicleModelDefinition.make == make,
VehicleModelDefinition.marketing_name == model,
VehicleModelDefinition.engine_code == gen
).order_by(VehicleModelDefinition.id)
result = await db.execute(stmt)
engines = result.scalars().all()
return engines
@staticmethod
async def get_user_vehicle_limit(db: AsyncSession, user_id: int, org_id: int) -> int:
"""
Get the vehicle limit for a user, checking both user-specific AND organization limits.
Returns the HIGHER value of the two as per requirements.
Args:
db: AsyncSession
user_id: User ID
org_id: Organization ID
Returns:
Maximum allowed vehicles (higher of user limit and organization limit)
"""
from app.models.identity import User
from app.services.config_service import config
try:
# Get user info
user_stmt = select(User).where(User.id == user_id)
user = (await db.execute(user_stmt)).scalar_one()
# Get global vehicle limits configuration
limits = await config.get_setting(db, "VEHICLE_LIMIT")
if limits is None:
logger.error(f"VEHICLE_LIMIT configuration not found in database for user {user_id}")
# Fallback to very high limit instead of restricting users
limits = {"admin": 9999, "superadmin": 9999, "user": 100, "free": 100, "premium": 100, "vip": 100, "service_pro": 100}
user_role = user.role.value if hasattr(user.role, 'value') else str(user.role)
subscription_plan = user.subscription_plan or "free"
# Get user-specific limit (based on role or subscription plan)
user_limit = limits.get(user_role)
if user_limit is None:
user_limit = limits.get(subscription_plan.lower(), 100)
# Get organization-specific limit (if configured)
org_limit = None
try:
org_limits = await config.get_setting(db, "VEHICLE_LIMIT", org_id=org_id)
if org_limits and isinstance(org_limits, dict):
# Organization might have different limit structure
# Try to get limit for user's role or use a default org limit
org_limit = org_limits.get(user_role) or org_limits.get(subscription_plan.lower())
if org_limit is None and "default" in org_limits:
org_limit = org_limits["default"]
except Exception as e:
logger.debug(f"No organization-specific VEHICLE_LIMIT found for org {org_id}: {e}")
org_limit = None
# Log the calculated limit for debugging
final_limit = user_limit
if org_limit is not None:
final_limit = max(user_limit, org_limit)
logger.info(f"Calculated limit for user {user_id} (role: {user_role}, plan: {subscription_plan}): user_limit={user_limit}, org_limit={org_limit}, final={final_limit}")
else:
logger.info(f"Calculated limit for user {user_id} (role: {user_role}, plan: {subscription_plan}): user_limit={user_limit}, org_limit=None, final={final_limit}")
return final_limit
except Exception as e:
logger.error(f"Error getting vehicle limit for user {user_id}, org {org_id}: {e}")
# Fallback to a reasonable default
return 100
@staticmethod
async def _award_first_car_badge(db: AsyncSession, user_id: int, org_id: int):
"""
Award 'First Car' badge to user if this is their first vehicle.
Checks if the user already has any vehicles in the organization.
If not, awards the 'First Car' badge.
"""
try:
from sqlalchemy import select, func
from app.models.gamification import Badge, UserBadge
# Check if user already has vehicles in this organization
from app.models.vehicle import Asset
vehicle_count_stmt = select(func.count(Asset.id)).where(
Asset.current_organization_id == org_id,
Asset.status == "active"
)
vehicle_count = (await db.execute(vehicle_count_stmt)).scalar()
# If this is the first vehicle (count should be 1 after the new one is added)
if vehicle_count == 1:
# Get or create the "First Car" badge
badge_stmt = select(Badge).where(Badge.name == "First Car")
badge_result = await db.execute(badge_stmt)
badge = badge_result.scalar_one_or_none()
if not badge:
# Create the badge if it doesn't exist
badge = Badge(
name="First Car",
description="Awarded for adding your first vehicle to the fleet",
icon_url="/badges/first-car.svg"
)
db.add(badge)
await db.flush()
# Check if user already has this badge
user_badge_stmt = select(UserBadge).where(
UserBadge.user_id == user_id,
UserBadge.badge_id == badge.id
)
user_badge_result = await db.execute(user_badge_stmt)
existing_user_badge = user_badge_result.scalar_one_or_none()
if not existing_user_badge:
# Award the badge to the user
user_badge = UserBadge(
user_id=user_id,
badge_id=badge.id,
awarded_at=datetime.utcnow()
)
db.add(user_badge)
await db.flush()
logger = logging.getLogger(__name__)
logger.info(f"Awarded 'First Car' badge to user {user_id}")
except Exception as e:
logger = logging.getLogger(__name__)
logger.error(f"Error awarding first car badge: {e}")
# Don't raise the error - badge awarding shouldn't break vehicle creation