2026-07-14 12:03:19 +02:00
|
|
|
"""SQLAlchemy models for vehicles and mobile.de listings."""
|
|
|
|
|
|
|
|
|
|
import enum
|
|
|
|
|
import uuid
|
|
|
|
|
from datetime import date, datetime
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import (
|
|
|
|
|
CheckConstraint,
|
|
|
|
|
Date,
|
|
|
|
|
DateTime,
|
|
|
|
|
Enum,
|
|
|
|
|
ForeignKey,
|
|
|
|
|
Integer,
|
|
|
|
|
Numeric,
|
|
|
|
|
String,
|
|
|
|
|
Text,
|
|
|
|
|
func,
|
|
|
|
|
)
|
|
|
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
|
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
|
|
|
|
|
|
from app.database import Base
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VehicleCondition(str, enum.Enum):
|
|
|
|
|
new = "new"
|
|
|
|
|
used = "used"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VehicleAvailability(str, enum.Enum):
|
|
|
|
|
available = "available"
|
|
|
|
|
reserved = "reserved"
|
|
|
|
|
sold = "sold"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VehicleType(str, enum.Enum):
|
|
|
|
|
lkw = "lkw"
|
|
|
|
|
pkw = "pkw"
|
|
|
|
|
baumaschine = "baumaschine"
|
|
|
|
|
stapler = "stapler"
|
|
|
|
|
transporter = "transporter"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SyncStatus(str, enum.Enum):
|
|
|
|
|
pending = "pending"
|
|
|
|
|
synced = "synced"
|
|
|
|
|
fehler = "fehler"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Vehicle(Base):
|
|
|
|
|
"""Vehicle entity for the ERP inventory."""
|
|
|
|
|
|
|
|
|
|
__tablename__ = "vehicles"
|
|
|
|
|
__table_args__ = (
|
|
|
|
|
CheckConstraint(
|
|
|
|
|
"char_length(fin) = 17", name="ck_vehicles_fin_length"
|
|
|
|
|
),
|
|
|
|
|
CheckConstraint(
|
|
|
|
|
"condition IN ('new', 'used')", name="ck_vehicles_condition"
|
|
|
|
|
),
|
|
|
|
|
CheckConstraint(
|
|
|
|
|
"availability IN ('available', 'reserved', 'sold')",
|
|
|
|
|
name="ck_vehicles_availability",
|
|
|
|
|
),
|
|
|
|
|
CheckConstraint(
|
|
|
|
|
"vehicle_type IN ('lkw', 'pkw', 'baumaschine', 'stapler', 'transporter')",
|
|
|
|
|
name="ck_vehicles_vehicle_type",
|
|
|
|
|
),
|
|
|
|
|
CheckConstraint(
|
|
|
|
|
"operating_hours_unit IS NULL OR operating_hours_unit IN ('h', 'min')",
|
|
|
|
|
name="ck_vehicles_operating_hours_unit",
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
primary_key=True,
|
|
|
|
|
default=uuid.uuid4,
|
|
|
|
|
)
|
|
|
|
|
make: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
|
|
|
model: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
|
|
|
fin: Mapped[str] = mapped_column(
|
|
|
|
|
String(17), unique=True, nullable=False, index=True
|
|
|
|
|
)
|
|
|
|
|
year: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
first_registration: Mapped[date | None] = mapped_column(
|
|
|
|
|
Date, nullable=True
|
|
|
|
|
)
|
|
|
|
|
power_kw: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
power_hp: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
fuel_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
|
|
|
transmission: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
|
|
|
color: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
|
|
|
condition: Mapped[str] = mapped_column(
|
|
|
|
|
String(20), nullable=False, default="used"
|
|
|
|
|
)
|
|
|
|
|
location: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
|
|
|
availability: Mapped[str] = mapped_column(
|
|
|
|
|
String(20), nullable=False, default="available"
|
|
|
|
|
)
|
|
|
|
|
price: Mapped[Decimal] = mapped_column(
|
|
|
|
|
Numeric(12, 2), nullable=False
|
|
|
|
|
)
|
|
|
|
|
vehicle_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
|
|
|
|
lkw_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
|
|
|
machine_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
|
|
|
body_type: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
|
|
|
operating_hours: Mapped[Decimal | None] = mapped_column(
|
|
|
|
|
Numeric(12, 1), nullable=True
|
|
|
|
|
)
|
|
|
|
|
operating_hours_unit: Mapped[str | None] = mapped_column(
|
|
|
|
|
String(5), nullable=True
|
|
|
|
|
)
|
|
|
|
|
mileage_km: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
|
description: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
|
|
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True),
|
|
|
|
|
nullable=False,
|
|
|
|
|
server_default=func.now(),
|
|
|
|
|
onupdate=func.now(),
|
|
|
|
|
)
|
|
|
|
|
deleted_at: Mapped[datetime | None] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
mobile_de_listings: Mapped[list["MobileDeListing"]] = relationship(
|
|
|
|
|
back_populates="vehicle", cascade="all, delete-orphan"
|
|
|
|
|
)
|
2026-07-17 01:38:48 +02:00
|
|
|
files: Mapped[list["File"]] = relationship(
|
|
|
|
|
"File", back_populates="vehicle", cascade="all, delete-orphan"
|
|
|
|
|
)
|
2026-07-14 12:03:19 +02:00
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return f"<Vehicle id={self.id} fin={self.fin} make={self.make}>"
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> dict:
|
|
|
|
|
"""Serialize vehicle for API responses."""
|
|
|
|
|
return {
|
|
|
|
|
"id": str(self.id),
|
|
|
|
|
"make": self.make,
|
|
|
|
|
"model": self.model,
|
|
|
|
|
"fin": self.fin,
|
|
|
|
|
"year": self.year,
|
|
|
|
|
"first_registration": (
|
|
|
|
|
self.first_registration.isoformat()
|
|
|
|
|
if self.first_registration
|
|
|
|
|
else None
|
|
|
|
|
),
|
|
|
|
|
"power_kw": self.power_kw,
|
|
|
|
|
"power_hp": self.power_hp,
|
|
|
|
|
"fuel_type": self.fuel_type,
|
|
|
|
|
"transmission": self.transmission,
|
|
|
|
|
"color": self.color,
|
|
|
|
|
"condition": self.condition,
|
|
|
|
|
"location": self.location,
|
|
|
|
|
"availability": self.availability,
|
|
|
|
|
"price": float(self.price) if self.price is not None else None,
|
|
|
|
|
"vehicle_type": self.vehicle_type,
|
|
|
|
|
"lkw_type": self.lkw_type,
|
|
|
|
|
"machine_type": self.machine_type,
|
|
|
|
|
"body_type": self.body_type,
|
|
|
|
|
"operating_hours": (
|
|
|
|
|
float(self.operating_hours)
|
|
|
|
|
if self.operating_hours is not None
|
|
|
|
|
else None
|
|
|
|
|
),
|
|
|
|
|
"operating_hours_unit": self.operating_hours_unit,
|
|
|
|
|
"mileage_km": self.mileage_km,
|
|
|
|
|
"description": self.description,
|
|
|
|
|
"created_at": (
|
|
|
|
|
self.created_at.isoformat() if self.created_at else None
|
|
|
|
|
),
|
|
|
|
|
"updated_at": (
|
|
|
|
|
self.updated_at.isoformat() if self.updated_at else None
|
|
|
|
|
),
|
|
|
|
|
"deleted_at": (
|
|
|
|
|
self.deleted_at.isoformat() if self.deleted_at else None
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MobileDeListing(Base):
|
|
|
|
|
"""mobile.de listing sync tracking for a vehicle."""
|
|
|
|
|
|
|
|
|
|
__tablename__ = "mobile_de_listings"
|
|
|
|
|
|
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
primary_key=True,
|
|
|
|
|
default=uuid.uuid4,
|
|
|
|
|
)
|
|
|
|
|
vehicle_id: Mapped[uuid.UUID] = mapped_column(
|
|
|
|
|
UUID(as_uuid=True),
|
|
|
|
|
ForeignKey("vehicles.id", ondelete="CASCADE"),
|
|
|
|
|
nullable=False,
|
|
|
|
|
index=True,
|
|
|
|
|
)
|
|
|
|
|
ad_id: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
|
|
|
sync_status: Mapped[str] = mapped_column(
|
|
|
|
|
String(20), nullable=False, default="pending"
|
|
|
|
|
)
|
|
|
|
|
synced_at: Mapped[datetime | None] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=True
|
|
|
|
|
)
|
|
|
|
|
error_log: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
|
|
|
)
|
|
|
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
|
|
|
DateTime(timezone=True),
|
|
|
|
|
nullable=False,
|
|
|
|
|
server_default=func.now(),
|
|
|
|
|
onupdate=func.now(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
vehicle: Mapped["Vehicle"] = relationship(back_populates="mobile_de_listings")
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return f"<MobileDeListing id={self.id} vehicle_id={self.vehicle_id} status={self.sync_status}>"
|
|
|
|
|
|
|
|
|
|
def to_dict(self) -> dict:
|
|
|
|
|
"""Serialize listing for API responses."""
|
|
|
|
|
return {
|
|
|
|
|
"id": str(self.id),
|
|
|
|
|
"vehicle_id": str(self.vehicle_id),
|
|
|
|
|
"ad_id": self.ad_id,
|
|
|
|
|
"sync_status": self.sync_status,
|
|
|
|
|
"synced_at": (
|
|
|
|
|
self.synced_at.isoformat() if self.synced_at else None
|
|
|
|
|
),
|
|
|
|
|
"error_log": self.error_log,
|
|
|
|
|
"created_at": (
|
|
|
|
|
self.created_at.isoformat() if self.created_at else None
|
|
|
|
|
),
|
|
|
|
|
"updated_at": (
|
|
|
|
|
self.updated_at.isoformat() if self.updated_at else None
|
|
|
|
|
),
|
|
|
|
|
}
|