"""DATEV CSV format helper (Buchungsstapel). Generates CSV in DATEV-compatible format with headers: Datum, Konto, Gegenkonto, Betrag, Belegfeld, Buchungstext """ import csv import io from datetime import date 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