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
+154
View File
@@ -0,0 +1,154 @@
"""DATEV export service: create exports, list exports, generate CSV."""
from __future__ import annotations
import os
import uuid
from datetime import date
from decimal import Decimal
from typing import Any
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.models.datev_export import DATEVExport
from app.models.sale import Sale
from app.utils.datev import generate_datev_csv, validate_datev_csv
async def create_export(
db: AsyncSession,
start_date: date,
end_date: date,
output_dir: str = "/tmp/datev_exports",
) -> DATEVExport:
"""Create a DATEV export for the given date range.
Queries all completed sales within the date range, generates a CSV file,
and stores the export record.
Args:
db: Async session.
start_date: Start of the date range (inclusive).
end_date: End of the date range (inclusive).
output_dir: Directory to save the CSV file.
Returns:
DATEVExport model instance.
"""
# Query completed sales within date range
stmt = (
select(Sale)
.options(
selectinload(Sale.vehicle),
selectinload(Sale.buyer),
selectinload(Sale.seller),
)
.where(
and_(
Sale.sale_date >= start_date,
Sale.sale_date <= end_date,
Sale.status == "completed",
)
)
.order_by(Sale.sale_date.asc())
)
result = await db.execute(stmt)
sales = list(result.scalars().all())
# Generate CSV content
csv_content = generate_datev_csv(sales)
# Calculate total amount
total_amount = sum(
(s.sale_price or Decimal("0")) for s in sales
)
# Save CSV file
os.makedirs(output_dir, exist_ok=True)
export_id = uuid.uuid4()
file_path = os.path.join(output_dir, f"datev_export_{export_id}.csv")
with open(file_path, "w", encoding="utf-8") as f:
f.write(csv_content)
# Create export record
export = DATEVExport(
id=export_id,
start_date=start_date,
end_date=end_date,
file_path=file_path,
total_amount=total_amount,
)
db.add(export)
await db.flush()
await db.refresh(export)
return export
async def list_exports(
db: AsyncSession,
page: int = 1,
page_size: int = 20,
) -> tuple[list[DATEVExport], int]:
"""List DATEV exports with pagination.
Returns (exports, total_count).
"""
count_stmt = select(func.count(DATEVExport.id))
total_result = await db.execute(count_stmt)
total = total_result.scalar_one()
data_stmt = (
select(DATEVExport)
.order_by(DATEVExport.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
result = await db.execute(data_stmt)
exports = list(result.scalars().all())
return exports, total
async def get_export_csv(db: AsyncSession, export_id: uuid.UUID) -> tuple[str, bytes] | None:
"""Get the CSV content for a DATEV export.
Args:
db: Async session.
export_id: Export ID.
Returns:
Tuple of (filename, csv_bytes) or None if export not found.
"""
stmt = select(DATEVExport).where(DATEVExport.id == export_id)
result = await db.execute(stmt)
export = result.scalar_one_or_none()
if export is None:
return None
if export.file_path and os.path.exists(export.file_path):
with open(export.file_path, "r", encoding="utf-8") as f:
csv_content = f.read()
else:
# Regenerate from sales if file is missing
sales_stmt = (
select(Sale)
.options(selectinload(Sale.vehicle))
.where(
and_(
Sale.sale_date >= export.start_date,
Sale.sale_date <= export.end_date,
Sale.status == "completed",
)
)
.order_by(Sale.sale_date.asc())
)
sales_result = await db.execute(sales_stmt)
sales = list(sales_result.scalars().all())
csv_content = generate_datev_csv(sales)
filename = f"datev_export_{export.id}.csv"
return filename, csv_content.encode("utf-8")