Files
erp-nutzfahrzeuge/backend/app/utils/datev.py
T

109 lines
3.0 KiB
Python
Raw Normal View History

"""DATEV CSV format helper (Buchungsstapel).
Generates CSV in DATEV-compatible format with headers:
Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext
"""
import csv
import io
from decimal import Decimal
from typing import Any
# DATEV Buchungsstapel CSV headers
DATEV_HEADERS = [
"Datum",
"Konto",
"Gegenkonto",
"Betrag",
"Belegfeld",
"Buchungstext",
]
def generate_datev_csv(sales: list[Any]) -> str:
"""Generate DATEV CSV content from a list of sale objects.
Each sale produces one booking row:
- Datum: sale_date in DD.MM.YYYY format
- Konto: buyer account (1200 = Forderungen aus Lieferungen/Leistungen)
- Gegenkonto: revenue account (8400 = Erlöse aus Warenverkäufen)
- Betrag: sale_price as decimal with comma separator
- Belegfeld: sale ID (short form)
- Buchungstext: vehicle make + model + FIN
Args:
sales: List of Sale model instances with nested vehicle and buyer.
Returns:
CSV string with DATEV headers and one row per sale.
"""
output = io.StringIO()
writer = csv.writer(output, delimiter=";", quoting=csv.QUOTE_MINIMAL)
# Write header row
writer.writerow(DATEV_HEADERS)
for sale in sales:
# Format date as DD.MM.YYYY (DATEV convention)
datum = sale.sale_date.strftime("%d.%m.%Y") if sale.sale_date else ""
# Account numbers (standard DATEV SKR03)
konto = "1200" # Forderungen aus LuL
gegenkonto = "8400" # Erlöse aus Warenverkäufen
# Amount with comma as decimal separator (DATEV convention)
betrag = _format_amount(sale.sale_price)
# Belegfeld: short sale ID (first 8 chars)
belegfeld = str(sale.id)[:8].upper()
# Buchungstext: vehicle description
vehicle = getattr(sale, "vehicle", None)
if vehicle:
buchungstext = f"{vehicle.make} {vehicle.model} {vehicle.fin}"
else:
buchungstext = f"Verkauf {str(sale.id)[:8]}"
writer.writerow([datum, konto, gegenkonto, betrag, belegfeld, buchungstext])
return output.getvalue()
def _format_amount(amount: Decimal) -> str:
"""Format a Decimal amount for DATEV CSV (comma as decimal separator)."""
if amount is None:
return "0,00"
# Convert to string with 2 decimal places, replace dot with comma
formatted = f"{amount:.2f}"
return formatted.replace(".", ",")
def validate_datev_csv(csv_content: str) -> bool:
"""Validate that CSV content has correct DATEV headers and structure.
Args:
csv_content: CSV string to validate.
Returns:
True if valid, False otherwise.
"""
if not csv_content or not csv_content.strip():
return False
reader = csv.reader(io.StringIO(csv_content), delimiter=";")
try:
header = next(reader)
except StopIteration:
return False
if header != DATEV_HEADERS:
return False
# Check at least that rows have 6 columns each
for row in reader:
if len(row) != 6:
return False
return True