328 lines
15 KiB
Python
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})"
|
|
)
|