admin felület különválasztva

This commit is contained in:
Roo
2026-06-24 11:29:45 +00:00
parent 71ef33bb85
commit 80a5d67f79
462 changed files with 87873 additions and 312 deletions

View File

@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
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, CostCategory, Organization
from app.schemas.asset_cost import AssetCostCreate
from app.schemas.asset_cost import AssetCostCreate, AssetCostUpdate
from datetime import datetime, timezone
logger = logging.getLogger(__name__)
@@ -257,6 +257,100 @@ async def list_all_expenses(
}
@router.put("/{expense_id}", status_code=200)
async def update_expense(
expense_id: UUID,
update: AssetCostUpdate,
db: AsyncSession = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
Update an existing expense record.
Only the fields provided in the request body will be updated.
The expense_id is the UUID of the existing AssetCost record.
The user must be a member of the organization that owns the expense.
"""
# Fetch the existing expense
stmt = select(AssetCost).where(AssetCost.id == expense_id)
result = await db.execute(stmt)
cost = result.scalar_one_or_none()
if not cost:
raise HTTPException(status_code=404, detail="Expense not found.")
# Verify user has access to the expense's organization
org_stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id,
OrganizationMember.organization_id == cost.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 organization that owns this expense."
)
# Build update dict from non-None fields
update_data = update.model_dump(exclude_unset=True)
if not update_data:
raise HTTPException(status_code=400, detail="No fields provided for update.")
# Map 'date' field to 'cost_date' column if present
if 'date' in update_data:
update_data['cost_date'] = update_data.pop('date')
# Handle mileage_at_cost and description → store in data JSONB
data_update = {}
if 'mileage_at_cost' in update_data:
data_update['mileage_at_cost'] = update_data.pop('mileage_at_cost')
if 'description' in update_data:
data_update['description'] = update_data.pop('description')
# Merge data_update into existing data JSONB
if data_update:
existing_data = dict(cost.data or {})
existing_data.update(data_update)
update_data['data'] = existing_data
# Apply updates
for field, value in update_data.items():
setattr(cost, field, value)
try:
await db.flush()
await db.commit()
await db.refresh(cost)
logger.info(f"Expense {expense_id} updated by user {current_user.id}: fields={list(update_data.keys())}")
return {
"status": "success",
"id": cost.id,
"asset_id": cost.asset_id,
"category_id": cost.category_id,
"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,
"expense_status": cost.status,
"date": cost.date.isoformat() if cost.date else None,
}
except Exception as e:
await db.rollback()
logger.error(f"Expense update error for {expense_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Hiba a költség módosításakor"
)
@router.get("/{asset_id}")
async def list_asset_expenses(
asset_id: UUID,
@@ -395,30 +489,30 @@ async def create_expense(
detail=f"DRAFT_LIMIT_REACHED: Draft vehicles are limited to {limit} expenses. This asset already has {expense_count} expenses."
)
# Determine organization_id: prefer explicit from payload, fallback to asset fields
organization_id = expense.organization_id or asset.current_organization_id or asset.owner_org_id
if not organization_id:
# B2C fallback: if the asset is owned by a person (not an org),
# try to find the current user's default organization
# P0 ARCHITECTURE FIX: organization_id MUST follow the Asset, not the user.
# The cost belongs to the asset's current organization (e.g., Aprilia garage = org 21).
# The vendor is stored separately via vendor_organization_id.
#
# Resolution priority:
# 1. asset.current_organization_id (the asset's current fleet/garage)
# 2. asset.owner_org_id (the asset's owning organization)
# 3. Fallback: user's active organization from membership
if asset.current_organization_id:
organization_id = asset.current_organization_id
elif asset.owner_org_id:
organization_id = asset.owner_org_id
else:
# Fallback: user's active organization from membership
org_stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id,
OrganizationMember.is_verified == True
OrganizationMember.status == "active"
).limit(1)
org_result = await db.execute(org_stmt)
org_member = org_result.scalar_one_or_none()
if org_member:
organization_id = org_member.organization_id
else:
# Last resort: any membership (even unverified)
org_stmt = select(OrganizationMember).where(
OrganizationMember.user_id == current_user.id
).limit(1)
org_result = await db.execute(org_stmt)
org_member = org_result.scalar_one_or_none()
if org_member:
organization_id = org_member.organization_id
else:
raise HTTPException(status_code=400, detail="Asset has no associated organization.")
raise HTTPException(status_code=400, detail="Asset has no associated organization.")
# ── RBAC PHASE 2: CAPABILITY-BASED AUTO-APPROVAL ──
# JSONB-alapú képesség-ellenőrzés a fleet.org_roles tábla permissions oszlopából