RABC fejlesztése és beélesítése hibák kijavításával
This commit is contained in:
@@ -20,7 +20,7 @@ router = APIRouter()
|
||||
# --- 🛡️ ADMIN JOGOSULTSÁG ELLENŐRZŐ ---
|
||||
async def check_admin_access(current_user: User = Depends(deps.get_current_active_user)):
|
||||
""" Csak Admin vagy Superadmin léphet be a Sentinel központba. """
|
||||
if current_user.role not in [UserRole.admin, UserRole.superadmin]:
|
||||
if current_user.role not in [UserRole.ADMIN, UserRole.SUPERADMIN]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Sentinel jogosultság szükséges a művelethez!"
|
||||
|
||||
@@ -386,6 +386,8 @@ class AssetEventCreate(BaseModel):
|
||||
cost_id: Optional[UUID] = Field(None, description="Associated cost record ID")
|
||||
cost_amount: Optional[float] = Field(None, gt=0, description="Cost amount in the given currency. If > 0, an AssetCost record is auto-created.")
|
||||
currency: Optional[str] = Field(None, min_length=3, max_length=3, description="Currency code (e.g. HUF, EUR). Defaults to HUF on backend.")
|
||||
status: Optional[str] = Field(None, description="Event status (DRAFT, MISSING_TECH_DATA, COMPLETED) - auto-set by backend")
|
||||
linked_expense_id: Optional[UUID] = Field(None, description="Associated AssetCost ID (bidirectional link)")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -402,6 +404,8 @@ class AssetEventResponse(BaseModel):
|
||||
cost_id: Optional[UUID] = None
|
||||
cost_amount: Optional[float] = Field(None, description="Auto-created cost amount")
|
||||
currency: Optional[str] = Field(None, description="Currency code (e.g. HUF)")
|
||||
status: str = "DRAFT"
|
||||
linked_expense_id: Optional[UUID] = None
|
||||
event_date: datetime
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
@@ -410,7 +414,10 @@ class AssetEventResponse(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def model_validate(cls, obj, **kwargs):
|
||||
"""Override to resolve cost_amount and currency from the related AssetCost."""
|
||||
"""Override to resolve cost_amount and currency from the related AssetCost.
|
||||
|
||||
GROSS-FIRST: cost_amount resolves from amount_gross (Bruttó) as primary source.
|
||||
"""
|
||||
data = {}
|
||||
if hasattr(obj, '__dict__'):
|
||||
# Copy ORM attributes
|
||||
@@ -422,7 +429,10 @@ class AssetEventResponse(BaseModel):
|
||||
cost = getattr(obj, 'cost', None)
|
||||
if cost is not None:
|
||||
if data.get('cost_amount') is None:
|
||||
data['cost_amount'] = float(cost.amount_net) if cost.amount_net is not None else None
|
||||
# GROSS-FIRST: prefer amount_gross as the source of truth
|
||||
data['cost_amount'] = float(cost.amount_gross) if cost.amount_gross is not None else (
|
||||
float(cost.amount_net) if cost.amount_net is not None else None
|
||||
)
|
||||
if data.get('currency') is None:
|
||||
data['currency'] = cost.currency
|
||||
return cls(**data)
|
||||
@@ -1,13 +1,38 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/schemas/asset_cost.py
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
class AssetCostBase(BaseModel):
|
||||
"""Base schema — matches the AssetCost SQLAlchemy model fields."""
|
||||
amount_net: Decimal
|
||||
"""Base schema — matches the AssetCost SQLAlchemy model fields.
|
||||
|
||||
GROSS-FIRST (Masterbook 2.0.1): amount_gross is the primary/required field.
|
||||
In EU accounting, the gross amount (Bruttó) is the absolute Source of Truth.
|
||||
amount_net is optional — if only gross is provided, net = gross (0% VAT).
|
||||
If vat_rate is also provided, net is calculated back from gross.
|
||||
"""
|
||||
amount_gross: Optional[Decimal] = Field(None, description="Bruttó összeg (ÁFA-val) — ELSŐDLEGES forrás, de lehet NULL legacy rekordoknál")
|
||||
amount_net: Optional[Decimal] = Field(None, description="Nettó összeg (ÁFA nélkül) — OPCIONÁLIS, ha nincs megadva, bruttóból számolva")
|
||||
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def safety_net_gross_from_net(cls, data: Any) -> Any:
|
||||
"""SAFETY NET (Graceful Degradation): If the incoming payload contains
|
||||
amount_net but amount_gross is missing/None, automatically copy amount_net
|
||||
to amount_gross. This protects against legacy/cached browsers that still
|
||||
send amount_net in their payloads.
|
||||
|
||||
GROSS-FIRST (Masterbook 2.0.1): The backend is strictly Gross-first.
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
has_net = data.get('amount_net') is not None
|
||||
has_gross = data.get('amount_gross') is not None
|
||||
if has_net and not has_gross:
|
||||
data['amount_gross'] = data['amount_net']
|
||||
return data
|
||||
vat_rate: Optional[Decimal] = Field(None, description="ÁFA kulcs (pl. 27.00)")
|
||||
currency: str = "HUF"
|
||||
date: datetime = Field(default_factory=datetime.now)
|
||||
invoice_number: Optional[str] = None
|
||||
@@ -16,15 +41,45 @@ class AssetCostBase(BaseModel):
|
||||
mileage_at_cost: Optional[int] = None # Stored inside data JSONB
|
||||
description: Optional[str] = None # Stored inside data JSONB
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_gross_first(self):
|
||||
"""Gross-First validation: ensure amount_gross is the primary source.
|
||||
|
||||
If amount_net is not provided but vat_rate is, calculate net from gross.
|
||||
If neither amount_net nor vat_rate is provided, net = gross (0% VAT).
|
||||
If amount_gross is None (legacy data), default to 0 to prevent 500 errors.
|
||||
"""
|
||||
gross = self.amount_gross
|
||||
net = self.amount_net
|
||||
vat = self.vat_rate
|
||||
|
||||
# Fault tolerance: if gross is None (legacy NULL), default to 0
|
||||
if gross is None:
|
||||
self.amount_gross = Decimal("0")
|
||||
gross = self.amount_gross
|
||||
|
||||
if net is None and vat is not None and vat > 0:
|
||||
# Net will be calculated in the service layer from gross & vat
|
||||
pass
|
||||
elif net is None:
|
||||
# No net and no vat → net = gross (0% VAT content)
|
||||
self.amount_net = gross
|
||||
self.vat_rate = Decimal("0")
|
||||
return self
|
||||
|
||||
class AssetCostCreate(AssetCostBase):
|
||||
asset_id: UUID
|
||||
organization_id: Optional[int] = None # Auto-resolved from asset if not provided
|
||||
status: Optional[str] = Field(None, description="Költség státusz (DRAFT, PENDING_APPROVAL, APPROVED) - auto-set by backend")
|
||||
linked_asset_event_id: Optional[UUID] = Field(None, description="Kapcsolódó AssetEvent ID (kétirányú hivatkozáshoz)")
|
||||
|
||||
class AssetCostResponse(AssetCostBase):
|
||||
"""Response schema — matches DB columns from vehicle.asset_costs."""
|
||||
id: UUID
|
||||
asset_id: UUID
|
||||
organization_id: int
|
||||
status: str = "DRAFT"
|
||||
linked_asset_event_id: Optional[UUID] = None
|
||||
|
||||
# Derived / enriched fields (not in DB model directly)
|
||||
category_name: Optional[str] = None # Resolved from CostCategory relationship
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/schemas/asset_event.py
|
||||
from pydantic import BaseModel, ConfigDict, Field, validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from typing import Optional, Dict, Any, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
@@ -17,6 +17,12 @@ class AssetEventTypeEnum(str, Enum):
|
||||
RECALL = "RECALL" # Visszahívás
|
||||
OTHER = "OTHER" # Egyéb
|
||||
|
||||
class AssetEventStatusEnum(str, Enum):
|
||||
"""Esemény státuszok a feldolgozási munkafolyamathoz."""
|
||||
DRAFT = "DRAFT"
|
||||
MISSING_TECH_DATA = "MISSING_TECH_DATA"
|
||||
COMPLETED = "COMPLETED"
|
||||
|
||||
class AssetEventCreate(BaseModel):
|
||||
"""Digitális Szervizkönyv esemény létrehozásához szükséges adatok."""
|
||||
event_type: AssetEventTypeEnum = Field(..., description="Esemény típusa")
|
||||
@@ -24,9 +30,10 @@ class AssetEventCreate(BaseModel):
|
||||
description: str = Field(..., min_length=1, max_length=1000, description="Esemény leírása")
|
||||
event_date: Optional[datetime] = Field(None, description="Esemény dátuma (alapértelmezett: most)")
|
||||
cost_id: Optional[UUID] = Field(None, description="Kapcsolódó költség rekord ID (opcionális)")
|
||||
|
||||
# RBAC ellenőrzés: csak a tulajdonos vagy operátor szervezet adhat hozzá eseményt
|
||||
# Ezt a service rétegben validáljuk, nem a sémában
|
||||
cost_amount: Optional[float] = Field(None, gt=0, description="Költség összege. Ha > 0, automatikusan létrejön egy AssetCost rekord.")
|
||||
currency: Optional[str] = Field(None, min_length=3, max_length=3, description="Valutakód (pl. HUF, EUR)")
|
||||
status: Optional[AssetEventStatusEnum] = Field(None, description="Esemény státusz (auto-set by backend)")
|
||||
linked_expense_id: Optional[UUID] = Field(None, description="Kapcsolódó AssetCost ID (kétirányú hivatkozáshoz)")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -41,6 +48,12 @@ class AssetEventResponse(BaseModel):
|
||||
odometer_reading: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
cost_id: Optional[UUID] = None
|
||||
cost_amount: Optional[float] = Field(None, description="Auto-created cost amount")
|
||||
currency: Optional[str] = Field(None, description="Currency code (e.g. HUF)")
|
||||
|
||||
# Új mezők
|
||||
status: str = "DRAFT"
|
||||
linked_expense_id: Optional[UUID] = None
|
||||
|
||||
event_date: datetime
|
||||
created_at: datetime
|
||||
@@ -52,6 +65,31 @@ class AssetEventResponse(BaseModel):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def model_validate(cls, obj, **kwargs):
|
||||
"""Override to resolve cost_amount and currency from the related AssetCost.
|
||||
|
||||
GROSS-FIRST: cost_amount resolves from amount_gross (Bruttó) as primary source.
|
||||
"""
|
||||
data = {}
|
||||
if hasattr(obj, '__dict__'):
|
||||
# Copy ORM attributes
|
||||
for field in cls.model_fields:
|
||||
if hasattr(obj, field):
|
||||
data[field] = getattr(obj, field)
|
||||
|
||||
# Resolve cost_amount and currency from the cost relationship
|
||||
cost = getattr(obj, 'cost', None)
|
||||
if cost is not None:
|
||||
if data.get('cost_amount') is None:
|
||||
# GROSS-FIRST: prefer amount_gross as the source of truth
|
||||
data['cost_amount'] = float(cost.amount_gross) if cost.amount_gross is not None else (
|
||||
float(cost.amount_net) if cost.amount_net is not None else None
|
||||
)
|
||||
if data.get('currency') is None:
|
||||
data['currency'] = cost.currency
|
||||
return cls(**data)
|
||||
|
||||
class AssetEventUpdate(BaseModel):
|
||||
"""Digitális Szervizkönyv esemény frissítéséhez szükséges adatok."""
|
||||
event_type: Optional[AssetEventTypeEnum] = Field(None, description="Esemény típusa")
|
||||
@@ -59,8 +97,11 @@ class AssetEventUpdate(BaseModel):
|
||||
description: Optional[str] = Field(None, min_length=1, max_length=1000, description="Esemény leírása")
|
||||
event_date: Optional[datetime] = Field(None, description="Esemény dátuma")
|
||||
cost_id: Optional[UUID] = Field(None, description="Kapcsolódó költség rekord ID")
|
||||
status: Optional[AssetEventStatusEnum] = Field(None, description="Esemény státusz")
|
||||
linked_expense_id: Optional[UUID] = Field(None, description="Kapcsolódó AssetCost ID")
|
||||
|
||||
@validator('description')
|
||||
@field_validator('description')
|
||||
@classmethod
|
||||
def validate_description(cls, v):
|
||||
if v is not None and len(v.strip()) == 0:
|
||||
raise ValueError('Description cannot be empty or whitespace only')
|
||||
|
||||
@@ -56,13 +56,12 @@ class OrganizationUpdate(BaseModel):
|
||||
address_street_name: Optional[str] = None
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: Optional[str] = None
|
||||
address_stairwell: Optional[str] = None
|
||||
address_floor: Optional[str] = None
|
||||
address_door: Optional[str] = None
|
||||
address_hrsz: Optional[str] = None
|
||||
# ── Crowdsourced search fields ──
|
||||
aliases: Optional[List[str]] = None
|
||||
tags: Optional[List[str]] = None
|
||||
# ── Subscription ──
|
||||
subscription_tier_id: Optional[int] = None
|
||||
|
||||
|
||||
class OrganizationResponse(BaseModel):
|
||||
@@ -79,10 +78,18 @@ class OrganizationResponse(BaseModel):
|
||||
is_active: bool = True
|
||||
is_deleted: bool = False
|
||||
subscription_plan: str = "FREE"
|
||||
subscription_tier_id: Optional[int] = None
|
||||
visual_settings: Optional[dict] = None
|
||||
notification_settings: Optional[Any] = None
|
||||
external_integration_config: Optional[Any] = None
|
||||
aliases: List[str] = Field(default_factory=list)
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
# ── Cím adatok (Address fields) ──
|
||||
address_zip: Optional[str] = None
|
||||
address_city: Optional[str] = None
|
||||
address_street_name: Optional[str] = None
|
||||
address_street_type: Optional[str] = None
|
||||
address_house_number: Optional[str] = None
|
||||
address_hrsz: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -1,7 +1,7 @@
|
||||
# /opt/docker/dev/service_finder/backend/app/schemas/user.py
|
||||
import uuid
|
||||
from pydantic import BaseModel, EmailStr, field_validator, ConfigDict
|
||||
from typing import Optional, Any
|
||||
from typing import Optional, Any, Dict
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
@@ -66,6 +66,10 @@ class UserResponse(UserBase):
|
||||
person: Optional[PersonResponse] = None
|
||||
# Utolsó admin flag (ha a user az egyetlen aktív admin bármely szervezetében)
|
||||
is_last_admin: bool = False
|
||||
# RBAC Phase 3: Rendszerszintű képességek (a SYSTEM_CAPABILITIES_MATRIX-ból)
|
||||
system_capabilities: Dict[str, bool] = {}
|
||||
# RBAC Phase 3: Szervezeti szintű képességek (org_id -> capability -> bool)
|
||||
org_capabilities: Dict[str, Dict[str, bool]] = {}
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user