2026.03.29 20:00 Gitea_manager javítás előtt
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user