2026-07-17 01:58:34 +02:00
|
|
|
"""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 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
|
2026-07-17 21:16:38 +02:00
|
|
|
from app.utils.datev import generate_datev_csv
|
2026-07-17 01:58:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|