101 lines
5.1 KiB
Python
Executable File
101 lines
5.1 KiB
Python
Executable File
# /opt/docker/dev/service_finder/backend/app/schemas/asset_cost.py
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
from typing import Optional, Dict, Any
|
|
from datetime import datetime, date
|
|
from decimal import Decimal
|
|
from uuid import UUID
|
|
|
|
class AssetCostBase(BaseModel):
|
|
"""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"
|
|
cost_date: datetime = Field(default_factory=lambda: datetime.now(), validation_alias="date", description="Költség rögzítésének dátuma/időpontja")
|
|
invoice_number: Optional[str] = None
|
|
data: Dict[str, Any] = Field(default_factory=dict) # nyugta adatai, GPS koordináták
|
|
category_id: int # FK a CostCategory táblához
|
|
mileage_at_cost: Optional[int] = None # Stored inside data JSONB
|
|
description: Optional[str] = None # Stored inside data JSONB
|
|
|
|
# === BESZÁLLÍTÓI ADATOK (B2B expansion) ===
|
|
vendor_organization_id: Optional[int] = Field(None, description="Beszállító szervezet ID (FK fleet.organizations)")
|
|
external_vendor_name: Optional[str] = Field(None, max_length=200, description="Szabadon gépelhető beszállító név (ha nincs a rendszerben)")
|
|
|
|
# === KÖNYVELÉSI DÁTUMOK ===
|
|
invoice_date: Optional[date] = Field(None, description="Számla kiállításának dátuma")
|
|
fulfillment_date: Optional[date] = Field(None, description="Teljesítés dátuma")
|
|
|
|
@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
|
|
category_code: Optional[str] = None # Resolved from CostCategory relationship
|
|
description: Optional[str] = None # Extracted from data['description']
|
|
mileage_at_cost: Optional[int] = None # Extracted from data['mileage_at_cost']
|
|
|
|
# Enriched vendor name (resolved from vendor_organization_id relationship)
|
|
vendor_name: Optional[str] = None # Resolved from Organization.name via vendor_organization_id
|
|
|
|
model_config = ConfigDict(from_attributes=True) |