Files
erp-nutzfahrzeuge/backend/app/models/vehicle.py
T

243 lines
8.0 KiB
Python
Raw Normal View History

"""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"
)
files: Mapped[list["File"]] = relationship(
"File", back_populates="vehicle", cascade="all, delete-orphan"
)
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
),
}