1f85277a3b
- neue Seitenleisten-Kategorie accounting (Finanzbuchhaltung / Financial accounting) - CSV (UTF-8, UTF-8 mit BOM, Windows-1252) pruefen, Vorschau mit Zeilenfehlern, DATEV-Lohn-ASCII-Export - Beraternummer, Mandantennummer, Lohnart je Mandant als Admin-Einstellung (KantineDatevConfig mit Zeilenschutz), anfangs leer - hochgeladene Daten werden nicht gespeichert; Download per Blob (auch im Desktop-Client) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
112 lines
3.3 KiB
TypeScript
112 lines
3.3 KiB
TypeScript
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_<Beraternr>_<Mandantennr>_<MM>_<YYYY>.sic */
|
|
export function buildKantineExportFilename(
|
|
settings: KantineDatevSettings,
|
|
abrechnungsMonat: string,
|
|
): string {
|
|
const [monat, jahr] = abrechnungsMonat.split('/');
|
|
return `LuG_${settings.beraterNr}_${settings.mandantNr}_${monat}_${jahr}.sic`;
|
|
}
|