Files
service-finder/backend/app/models/fleet_finance/models.py
2026-06-24 11:29:45 +00:00

328 lines
15 KiB
Python

# /opt/docker/dev/service_finder/backend/app/models/fleet_finance/models.py
"""
fleet_finance schema: Flotta pénzügyi adatok.
- CostCategory: Standardizált költségkategóriák hierarchiája (áthelyezve vehicle -> fleet_finance)
- AssetCost: Eszközhöz kapcsolódó tényleges költségnapló (áthelyezve vehicle -> fleet_finance)
- AssetFinancials: Beszerzés és értékcsökkenés (áthelyezve vehicle -> fleet_finance, kibővítve finanszírozási adatokkal)
- InsuranceProvider: Biztosítók katalógusa (ÚJ)
- VehicleInsurancePolicy: Jármű biztosítási kötvények (ÚJ)
- VehicleTaxObligation: Jármű adókötelezettségek (ÚJ)
"""
from __future__ import annotations
import uuid
import enum
from datetime import datetime, date
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import (
String, Boolean, DateTime, ForeignKey, Numeric, text, Text,
UniqueConstraint, Integer, Date
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID as PG_UUID, JSONB
from sqlalchemy.sql import func
from app.database import Base
if TYPE_CHECKING:
from app.models.vehicle.asset import Asset
from app.models.marketplace.organization import Organization
from app.models.system.document import Document
# =============================================================================
# CostCategory (áthelyezve vehicle -> fleet_finance)
# =============================================================================
class CostCategory(Base):
"""
Standardizált költségkategóriák hierarchikus fája.
Rendszerkategóriák (is_system=True) nem törölhetők, csak felhasználói kategóriák.
Áthelyezve a vehicle sémából a fleet_finance sémába.
"""
__tablename__ = "cost_categories"
__table_args__ = {"schema": "fleet_finance", "extend_existing": True}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
parent_id: Mapped[Optional[int]] = mapped_column(
Integer,
ForeignKey("fleet_finance.cost_categories.id", ondelete="SET NULL"),
nullable=True,
index=True
)
code: Mapped[str] = mapped_column(String(50), unique=True, index=True, nullable=False)
name: Mapped[str] = mapped_column(String(100), nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
is_system: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false")
visibility: Mapped[str] = mapped_column(String(20), default="both", server_default="'both'")
min_tier: Mapped[str] = mapped_column(String(20), default="free", server_default="'free'")
accounting_code: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
# ── P0 3D CAPABILITY MATRIX: required_feature ──
# If set, this category is ONLY visible to organizations whose resolved
# capability matrix has this feature key set to True.
# Example: "can_use_analytics" → only orgs with that capability see this category.
required_feature: Mapped[Optional[str]] = mapped_column(
String(50),
nullable=True,
comment="Feature key required to access this cost category"
)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
# Hierarchikus kapcsolatok
parent: Mapped[Optional["CostCategory"]] = relationship(
"CostCategory",
remote_side=[id],
back_populates="children",
foreign_keys=[parent_id]
)
children: Mapped[list["CostCategory"]] = relationship(
"CostCategory",
back_populates="parent",
foreign_keys=[parent_id]
)
# Kapcsolódó költségek (AssetCost)
asset_costs: Mapped[list["AssetCost"]] = relationship("AssetCost", back_populates="category")
def __repr__(self) -> str:
return f"CostCategory(id={self.id}, code='{self.code}', name='{self.name}')"
# =============================================================================
# AssetCostStatusEnum
# =============================================================================
class AssetCostStatusEnum(str, enum.Enum):
"""Költség státuszok a jóváhagyási munkafolyamathoz."""
DRAFT = "DRAFT"
PENDING_APPROVAL = "PENDING_APPROVAL"
APPROVED = "APPROVED"
# =============================================================================
# AssetCost (áthelyezve vehicle -> fleet_finance)
# =============================================================================
class AssetCost(Base):
""" II. Üzemeltetés és TCO kimutatás. Áthelyezve a vehicle sémából. """
__tablename__ = "asset_costs"
__table_args__ = {"schema": "fleet_finance"}
id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False)
organization_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet.organizations.id"), nullable=False)
category_id: Mapped[int] = mapped_column(Integer, ForeignKey("fleet_finance.cost_categories.id"), index=True, nullable=False)
amount_net: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
amount_gross: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
vat_rate: Mapped[Optional[float]] = mapped_column(Numeric(5, 2), nullable=True)
currency: Mapped[str] = mapped_column(String(3), default="HUF")
date: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
invoice_number: Mapped[Optional[str]] = mapped_column(String(100), index=True)
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("system.documents.id"), nullable=True)
# Státusz a jóváhagyási munkafolyamathoz
status: Mapped[str] = mapped_column(String(30), default="DRAFT", server_default=text("'DRAFT'"), index=True)
# Kétirányú hivatkozás az AssetEvent felé (kettős könyvelés elkerülése)
linked_asset_event_id: Mapped[Optional[uuid.UUID]] = mapped_column(
PG_UUID(as_uuid=True),
ForeignKey("vehicle.asset_events.id"),
nullable=True,
index=True
)
# === BESZÁLLÍTÓI HIVATKOZÁSOK (B2B expansion) ===
# Hivatkozás a fleet.organizations táblában lévő beszállítóra
vendor_organization_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("fleet.organizations.id"), nullable=True, index=True
)
# Szabadon gépelhető beszállító név (ha nincs a rendszerben)
external_vendor_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
# === KÖNYVELÉSI DÁTUMOK ===
invoice_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
fulfillment_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
data: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
asset: Mapped["Asset"] = relationship("Asset", back_populates="costs")
organization: Mapped["Organization"] = relationship("Organization", foreign_keys=[organization_id])
category: Mapped["CostCategory"] = relationship("CostCategory", back_populates="asset_costs")
# Kapcsolat a hivatkozott eseményhez
linked_event: Mapped[Optional["AssetEvent"]] = relationship(
"AssetEvent",
foreign_keys=[linked_asset_event_id],
post_update=True
)
# Beszállító kapcsolat
vendor_organization: Mapped[Optional["Organization"]] = relationship(
"Organization", foreign_keys=[vendor_organization_id]
)
# =============================================================================
# AssetFinancials (áthelyezve vehicle -> fleet_finance, kibővítve)
# =============================================================================
class AssetFinancials(Base):
"""
I. Beszerzés és IV. Értékcsökkenés (Amortizáció).
Áthelyezve a vehicle sémából a fleet_finance sémába.
Kibővítve finanszírozási adatokkal (lízing, hitel).
"""
__tablename__ = "asset_financials"
__table_args__ = {"schema": "fleet_finance"}
id: Mapped[int] = mapped_column(Integer, primary_key=True)
asset_id: Mapped[uuid.UUID] = mapped_column(PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), unique=True)
# === BESZERZÉS ===
purchase_price_net: Mapped[float] = mapped_column(Numeric(18, 2))
purchase_price_gross: Mapped[float] = mapped_column(Numeric(18, 2))
vat_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=27.00)
activation_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
verified_purchase_date: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True)
financing_type: Mapped[str] = mapped_column(String(50))
accounting_details: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
# === FINANSZÍROZÁS (ÚJ) ===
down_payment: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
monthly_installment: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
residual_value: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
contract_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
financing_provider: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
interest_rate: Mapped[Optional[float]] = mapped_column(Numeric(5, 2), nullable=True)
total_contract_value: Mapped[Optional[float]] = mapped_column(Numeric(18, 2), nullable=True)
lease_start_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
lease_end_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
# === KAPCSOLATOK ===
asset: Mapped["Asset"] = relationship("Asset", back_populates="financials")
# =============================================================================
# InsuranceProvider (ÚJ)
# =============================================================================
class InsuranceProvider(Base):
"""
Biztosítók katalógusa.
Tárolja a biztosítók elérhetőségeit és szolgáltatásait.
"""
__tablename__ = "insurance_providers"
__table_args__ = {"schema": "fleet_finance"}
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name: Mapped[str] = mapped_column(String(150), nullable=False, index=True)
country_code: Mapped[Optional[str]] = mapped_column(String(2), index=True, nullable=True, comment="ISO 3166-1 alpha-2 országkód (pl. 'HU'). Ha NULL, akkor globális.")
claim_phone: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
claim_url: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
services_offered: Mapped[dict] = mapped_column(JSONB, server_default=text("'{}'::jsonb"))
is_active: Mapped[bool] = mapped_column(Boolean, default=True, server_default=text("true"))
# Kapcsolatok
policies: Mapped[List["VehicleInsurancePolicy"]] = relationship(
"VehicleInsurancePolicy", back_populates="provider"
)
def __repr__(self) -> str:
return f"InsuranceProvider(id={self.id}, name='{self.name}')"
# =============================================================================
# VehicleInsurancePolicy (ÚJ)
# =============================================================================
class VehicleInsurancePolicy(Base):
"""
Jármű biztosítási kötvények.
Minden kötvény egy járműhöz (asset) és egy biztosítóhoz (provider) tartozik.
"""
__tablename__ = "vehicle_insurance_policies"
__table_args__ = {"schema": "fleet_finance"}
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
asset_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False, index=True
)
provider_id: Mapped[int] = mapped_column(
Integer, ForeignKey("fleet_finance.insurance_providers.id"), nullable=False, index=True
)
insurance_type: Mapped[str] = mapped_column(
String(20), nullable=False, comment="KGFB, CASCO"
)
policy_number: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
start_date: Mapped[date] = mapped_column(Date, nullable=False)
expiry_date: Mapped[date] = mapped_column(Date, nullable=False)
premium_amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
currency: Mapped[str] = mapped_column(String(3), default="HUF")
# Dokumentum kapcsolat (PDF kötvény)
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("system.documents.id"), nullable=True
)
# Kapcsolatok
asset: Mapped["Asset"] = relationship("Asset")
provider: Mapped["InsuranceProvider"] = relationship(
"InsuranceProvider", back_populates="policies"
)
document: Mapped[Optional["Document"]] = relationship("Document")
def __repr__(self) -> str:
return (
f"VehicleInsurancePolicy(id={self.id}, asset_id={self.asset_id}, "
f"type='{self.insurance_type}', policy='{self.policy_number}')"
)
# =============================================================================
# VehicleTaxObligation (ÚJ)
# =============================================================================
class VehicleTaxObligation(Base):
"""
Jármű adókötelezettségek.
Tárolja a különböző típusú adókat (súlyadó, cégautóadó, stb.).
"""
__tablename__ = "vehicle_tax_obligations"
__table_args__ = {"schema": "fleet_finance"}
id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
asset_id: Mapped[uuid.UUID] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("vehicle.assets.id"), nullable=False, index=True
)
tax_type: Mapped[str] = mapped_column(
String(30), nullable=False, comment="WEIGHT_TAX, COMPANY_CAR_TAX"
)
tax_year: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
currency: Mapped[str] = mapped_column(String(3), default="HUF")
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)
payment_status: Mapped[str] = mapped_column(
String(20), default="pending", server_default=text("'pending'")
)
# Dokumentum kapcsolat (PDF adóbevallás)
document_id: Mapped[Optional[uuid.UUID]] = mapped_column(
PG_UUID(as_uuid=True), ForeignKey("system.documents.id"), nullable=True
)
# Kapcsolatok
asset: Mapped["Asset"] = relationship("Asset")
document: Mapped[Optional["Document"]] = relationship("Document")
def __repr__(self) -> str:
return (
f"VehicleTaxObligation(id={self.id}, asset_id={self.asset_id}, "
f"type='{self.tax_type}', year={self.tax_year})"
)