import type { DatevRecord, KantineDatevSettings, KantinenRawRow } from './kantine-datev.types'; const SPALTEN_ANZAHL = 11; const TAB = '\t'; const CRLF = '\r\n'; /** * Transformiert einen Betrag gemaess DATEV-Regel: Komma -> Punkt, immer * negativ, exakt 2 Nachkommastellen. "7,94" -> "-7.94", "51,5" -> "-51.50". */ export function transformBetrag(betrag: string): string { const cleaned = betrag.trim().replace(',', '.'); const num = parseFloat(cleaned); const absValue = Math.abs(num); return `-${absValue.toFixed(2)}`; } export function transformToDatevRecords(rows: KantinenRawRow[]): DatevRecord[] { return rows.map((row) => ({ personalNr: row.personalNr.trim(), betrag: transformBetrag(row.betrag), })); } /** * Generiert die komplette DATEV-Lohn-ASCII-Datei. * * Kopf: Beraternr TAB Mandantennr TAB MM/YYYY + 8 leere Spalten * Detail: TAB PersonalNr TAB TAB Lohnart TAB -Betrag + 6 leere Spalten * * Exakt 11 Spalten je Zeile, CRLF-Zeilenenden, die Datei endet mit CRLF. * Berater-, Mandantennummer und Lohnart kommen aus den Einstellungen des * Mandanten, nicht aus festen Werten. */ export function generateDatevOutput( records: DatevRecord[], abrechnungsMonat: string, settings: KantineDatevSettings, ): string { const lines: string[] = []; lines.push( [settings.beraterNr, settings.mandantNr, abrechnungsMonat, '', '', '', '', '', '', '', ''].join( TAB, ), ); for (const record of records) { lines.push( ['', record.personalNr, '', settings.lohnart, record.betrag, '', '', '', '', '', ''].join( TAB, ), ); } return lines.join(CRLF) + CRLF; } /** * Qualitaetspruefung der erzeugten Ausgabe: 11 Spalten je Zeile, nur CRLF, * Datei endet mit CRLF, alle Betraege negativ mit 2 Nachkommastellen. */ export function qualityCheck(output: string): { passed: boolean; errors: string[] } { const errors: string[] = []; if (!output.endsWith(CRLF)) { errors.push('Datei endet nicht mit CRLF'); } const ohneCarriageReturn = output.replace(/\r\n/g, ''); if (ohneCarriageReturn.includes('\n')) { errors.push('Datei enthaelt einzelne LF-Zeilenenden (nur CRLF erlaubt)'); } const zeilen = output.split(CRLF); const inhaltZeilen = zeilen.slice(0, -1); if (inhaltZeilen.length === 0) { errors.push('Datei enthaelt keine Zeilen'); return { passed: false, errors }; } for (let i = 0; i < inhaltZeilen.length; i++) { const spalten = inhaltZeilen[i].split(TAB); if (spalten.length !== SPALTEN_ANZAHL) { errors.push(`Zeile ${i + 1}: ${spalten.length} Spalten gefunden, ${SPALTEN_ANZAHL} erwartet`); } } const betragRegex = /^-\d+\.\d{2}$/; for (let i = 1; i < inhaltZeilen.length; i++) { const spalten = inhaltZeilen[i].split(TAB); const betrag = spalten[4]; if (betrag && !betragRegex.test(betrag)) { errors.push( `Zeile ${i + 1}: Betrag "${betrag}" ist nicht im Format -X.XX (negativ, 2 Nachkommastellen)`, ); } } return { passed: errors.length === 0, errors }; } /** Dateiname des Downloads: LuG____.sic */ export function buildKantineExportFilename( settings: KantineDatevSettings, abrechnungsMonat: string, ): string { const [monat, jahr] = abrechnungsMonat.split('/'); return `LuG_${settings.beraterNr}_${settings.mandantNr}_${monat}_${jahr}.sic`; }