felhasználói felületre pü
This commit is contained in:
@@ -42,7 +42,7 @@ from app.models.vehicle.asset import Asset
|
||||
from app.models.vehicle.asset import AssetEvent
|
||||
from app.models.vehicle.asset import OdometerReading
|
||||
from app.models.marketplace.organization import Organization
|
||||
from app.models.marketplace.organization import OrganizationMember
|
||||
from app.models.marketplace.organization import OrganizationMember, OrgRole
|
||||
from app.models.identity.social import ServiceProvider
|
||||
from app.models.marketplace.service import ServiceProfile, ServiceExpertise
|
||||
from app.models.system.system import SystemParameter
|
||||
@@ -54,6 +54,123 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ── SINGLE EXPENSE READ ENDPOINT ──
|
||||
# P0 BUGFIX (2026-07-26): Dedicated single-expense endpoint that returns
|
||||
# enriched AssetCostResponse with resolved category name, parent_category_id,
|
||||
# provider name, etc. This is used by the frontend edit modal hydration.
|
||||
#
|
||||
# NOTE: This MUST be registered BEFORE the {asset_id} wildcard route to avoid
|
||||
# FastAPI routing the "by-id" path segment as an asset_id UUID.
|
||||
|
||||
|
||||
@router.get("/by-id/{expense_id}")
|
||||
async def get_expense_by_id(
|
||||
expense_id: uuid.UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user=Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get a single expense by its ID with enriched fields.
|
||||
|
||||
Returns a fully populated AssetCostResponse including:
|
||||
- category_name, category_code, parent_category_id (from CostCategory JOIN)
|
||||
- service_provider_name (from ServiceProvider JOIN)
|
||||
- vendor_name (from Organization JOIN via vendor_organization_id)
|
||||
- description, mileage_at_cost (extracted from data JSONB)
|
||||
|
||||
This is the preferred endpoint for frontend edit modal hydration.
|
||||
"""
|
||||
from app.models.fleet_finance.models import CostCategory
|
||||
from app.models.identity.social import ServiceProvider
|
||||
from app.models.marketplace.organization import Organization
|
||||
|
||||
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.")
|
||||
|
||||
# Resolve enriched fields via explicit queries (no lazy loading surprises)
|
||||
# 1. Category details + parent_id
|
||||
category_name = None
|
||||
category_code = None
|
||||
parent_category_id = None
|
||||
if cost.category_id is not None:
|
||||
cat_stmt = select(CostCategory).where(CostCategory.id == cost.category_id)
|
||||
cat_result = await db.execute(cat_stmt)
|
||||
category = cat_result.scalar_one_or_none()
|
||||
if category:
|
||||
category_name = category.name
|
||||
category_code = category.code
|
||||
parent_category_id = category.parent_id
|
||||
|
||||
# 2. Service provider name
|
||||
service_provider_name = None
|
||||
if cost.service_provider_id is not None:
|
||||
sp_stmt = select(ServiceProvider).where(ServiceProvider.id == cost.service_provider_id)
|
||||
sp_result = await db.execute(sp_stmt)
|
||||
provider = sp_result.scalar_one_or_none()
|
||||
if provider:
|
||||
service_provider_name = provider.name
|
||||
|
||||
# 3. Vendor organization name
|
||||
vendor_name = None
|
||||
if cost.vendor_organization_id is not None:
|
||||
org_stmt = select(Organization).where(Organization.id == cost.vendor_organization_id)
|
||||
org_result = await db.execute(org_stmt)
|
||||
vendor_org = org_result.scalar_one_or_none()
|
||||
if vendor_org:
|
||||
vendor_name = vendor_org.name
|
||||
|
||||
# 4. Extract description and mileage from data JSONB
|
||||
data = cost.data or {}
|
||||
description = data.get("description")
|
||||
mileage_at_cost = data.get("mileage_at_cost")
|
||||
|
||||
response = AssetCostResponse(
|
||||
id=cost.id,
|
||||
asset_id=cost.asset_id,
|
||||
organization_id=cost.organization_id,
|
||||
category_id=cost.category_id,
|
||||
status=cost.status,
|
||||
linked_asset_event_id=cost.linked_asset_event_id,
|
||||
# Monetary
|
||||
amount_gross=cost.amount_gross,
|
||||
amount_net=cost.amount_net,
|
||||
vat_rate=cost.vat_rate,
|
||||
currency=cost.currency,
|
||||
# Dates
|
||||
cost_date=cost.date,
|
||||
invoice_number=cost.invoice_number,
|
||||
# Invoice dates
|
||||
invoice_date=cost.invoice_date,
|
||||
fulfillment_date=cost.fulfillment_date,
|
||||
# Vendor fields
|
||||
vendor_organization_id=cost.vendor_organization_id,
|
||||
service_provider_id=cost.service_provider_id,
|
||||
external_vendor_name=cost.external_vendor_name,
|
||||
# Raw data JSONB
|
||||
data=data,
|
||||
# Enriched fields
|
||||
category_name=category_name,
|
||||
category_code=category_code,
|
||||
parent_category_id=parent_category_id,
|
||||
description=description,
|
||||
mileage_at_cost=mileage_at_cost,
|
||||
vendor_name=vendor_name,
|
||||
service_provider_name=service_provider_name,
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": response,
|
||||
}
|
||||
|
||||
# ── FUEL CATEGORY IDS ──
|
||||
# These are the category IDs that represent fuel costs.
|
||||
# Used for auto-approval logic: fuel costs are always auto-approved.
|
||||
@@ -100,22 +217,34 @@ async def _check_org_capability(
|
||||
|
||||
Uses the JSONB permissions field from fleet.org_roles.
|
||||
Returns True if the user's role has the capability, False otherwise.
|
||||
"""
|
||||
from app.models.fleet.org_role import OrgRole
|
||||
|
||||
stmt = (
|
||||
select(OrgRole.permissions)
|
||||
.join(OrganizationMember, OrganizationMember.role == OrgRole.name)
|
||||
.where(
|
||||
OrganizationMember.user_id == user_id,
|
||||
OrganizationMember.organization_id == organization_id,
|
||||
OrganizationMember.status == "active",
|
||||
)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
row = result.scalar_one_or_none()
|
||||
if row and isinstance(row, dict):
|
||||
return row.get(capability, False)
|
||||
NOTE: Two-step query (not JOIN) to avoid PG ENUM vs varchar type mismatch.
|
||||
OrganizationMember.role is PG ENUM 'fleet.orguserrole', OrgRole.name_key is varchar.
|
||||
A direct JOIN would cause: operator does not exist: fleet.orguserrole = character varying
|
||||
See assets.py:_get_user_permissions() for the same pattern.
|
||||
"""
|
||||
# Step 1: Get the user's role name (Python string — asyncpg auto-converts ENUM)
|
||||
member_stmt = select(OrganizationMember.role).where(
|
||||
OrganizationMember.user_id == user_id,
|
||||
OrganizationMember.organization_id == organization_id,
|
||||
OrganizationMember.status == "active",
|
||||
).limit(1)
|
||||
member_result = await db.execute(member_stmt)
|
||||
org_role_name = member_result.scalar_one_or_none()
|
||||
|
||||
if not org_role_name:
|
||||
return False
|
||||
|
||||
# Step 2: Look up permissions from fleet.org_roles using the role name
|
||||
role_stmt = select(OrgRole.permissions).where(
|
||||
OrgRole.name_key == org_role_name,
|
||||
OrgRole.is_active == True,
|
||||
).limit(1)
|
||||
role_result = await db.execute(role_stmt)
|
||||
permissions = role_result.scalar_one_or_none()
|
||||
|
||||
if permissions and isinstance(permissions, dict):
|
||||
return permissions.get(capability, False)
|
||||
return False
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user