feat(T06): Verkaufsmodul + Rechtsdokumente + DATEV-Export + Sales UI

This commit is contained in:
2026-07-17 01:58:34 +02:00
parent a38d340ddc
commit 6603e411e9
33 changed files with 3481 additions and 107 deletions
+27
View File
@@ -0,0 +1,27 @@
"""SQLAlchemy models package.
Import all models here so they register with Base.metadata
before create_all/drop_all is called.
"""
from app.models.contact import Contact, ContactPerson
from app.models.datev_export import DATEVExport
from app.models.file import File
from app.models.ocr_result import OCRResult
from app.models.sale import Sale, SaleStatus
from app.models.user import User, UserRole
from app.models.vehicle import MobileDeListing, Vehicle
__all__ = [
"Contact",
"ContactPerson",
"DATEVExport",
"File",
"OCRResult",
"Sale",
"SaleStatus",
"User",
"UserRole",
"Vehicle",
"MobileDeListing",
]
+50
View File
@@ -0,0 +1,50 @@
"""SQLAlchemy model for DATEV export records."""
import uuid
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import Date, DateTime, Numeric, String, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class DATEVExport(Base):
"""DATEV export record tracking CSV exports for accounting."""
__tablename__ = "datev_exports"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
)
start_date: Mapped[date] = mapped_column(Date, nullable=False)
end_date: Mapped[date] = mapped_column(Date, nullable=False)
file_path: Mapped[str | None] = mapped_column(
String(500), nullable=True
)
total_amount: Mapped[Decimal] = mapped_column(
Numeric(14, 2), nullable=False, default=0
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
def __repr__(self) -> str:
return f"<DATEVExport id={self.id} start={self.start_date} end={self.end_date}>"
def to_dict(self) -> dict:
"""Serialize DATEV export for API responses."""
return {
"id": str(self.id),
"start_date": self.start_date.isoformat() if self.start_date else None,
"end_date": self.end_date.isoformat() if self.end_date else None,
"file_path": self.file_path,
"total_amount": (
float(self.total_amount) if self.total_amount is not None else None
),
"created_at": self.created_at.isoformat() if self.created_at else None,
}
+120
View File
@@ -0,0 +1,120 @@
"""SQLAlchemy model for sales (Verkauf)."""
import enum
import uuid
from datetime import date, datetime
from decimal import Decimal
from sqlalchemy import (
Boolean,
CheckConstraint,
Date,
DateTime,
ForeignKey,
Numeric,
String,
func,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class SaleStatus(str, enum.Enum):
draft = "draft"
completed = "completed"
cancelled = "cancelled"
class Sale(Base):
"""Sale entity linking a vehicle to a buyer (and optionally a seller)."""
__tablename__ = "sales"
__table_args__ = (
CheckConstraint(
"status IN ('draft', 'completed', 'cancelled')",
name="ck_sales_status",
),
)
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="RESTRICT"),
nullable=False,
index=True,
)
buyer_contact_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("contacts.id", ondelete="RESTRICT"),
nullable=False,
index=True,
)
seller_contact_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True),
ForeignKey("contacts.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
sale_price: Mapped[Decimal] = mapped_column(
Numeric(12, 2), nullable=False
)
sale_date: Mapped[date] = mapped_column(
Date, nullable=False, default=func.current_date()
)
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="draft"
)
is_gwg: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False
)
contract_pdf_path: Mapped[str | None] = mapped_column(
String(500), 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(lazy="selectin")
buyer: Mapped["Contact"] = relationship(
"Contact",
foreign_keys=[buyer_contact_id],
lazy="selectin",
)
seller: Mapped["Contact | None"] = relationship(
"Contact",
foreign_keys=[seller_contact_id],
lazy="selectin",
)
def __repr__(self) -> str:
return f"<Sale id={self.id} vehicle_id={self.vehicle_id} status={self.status}>"
def to_dict(self) -> dict:
"""Serialize sale for API responses."""
return {
"id": str(self.id),
"vehicle_id": str(self.vehicle_id),
"buyer_contact_id": str(self.buyer_contact_id),
"seller_contact_id": (
str(self.seller_contact_id) if self.seller_contact_id else None
),
"sale_price": float(self.sale_price) if self.sale_price is not None else None,
"sale_date": self.sale_date.isoformat() if self.sale_date else None,
"status": self.status,
"is_gwg": self.is_gwg,
"contract_pdf_path": self.contract_pdf_path,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}