feat(handelsware-datev): API, Kontenliste mit Zeilenschutz und DATEV-Export
- Prisma-Modelle HandelswareDatevConfig und HandelswareKonto mit Zeilenschutz (Migration 20261002130000) - XLSX lesen (B1 Kopf, A/B ab Zeile 2, Zahl oder deutscher Text), Konten zuordnen, TXT erzeugen - neue Konten werden nur beim Export in einer mandantengebundenen Transaktion gespeichert (409 bei geaenderter Liste) - Konten-CSV Import (alles ersetzen) und Export mit Schutz vor Formeleinschleusung - Einstellungen nur fuer Administratoren, statische Routen vor accounts/:id Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { generateKontenCsv, parseKontenCsv } from './handelsware-konten-csv';
|
||||
|
||||
const csv = (text: string, enc: BufferEncoding = 'utf8') => Buffer.from(text, enc);
|
||||
|
||||
describe('parseKontenCsv', () => {
|
||||
it('liest CRLF und LF gleich', () => {
|
||||
const a = parseKontenCsv(csv('Kaffee;2010;4000\r\nTee;2011;4001\r\n'), 4711);
|
||||
const b = parseKontenCsv(csv('Kaffee;2010;4000\nTee;2011;4001'), 4711);
|
||||
expect(a.accounts).toEqual(b.accounts);
|
||||
expect(a.accounts).toHaveLength(2);
|
||||
expect(a.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('dekodiert Windows-1252 und UTF-8 mit BOM', () => {
|
||||
expect(parseKontenCsv(csv('Käse;2010;4000', 'latin1'), null).accounts[0].name).toBe('Käse');
|
||||
const bom = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), csv('Käse;2010;4000')]);
|
||||
expect(parseKontenCsv(bom, null).accounts[0].name).toBe('Käse');
|
||||
});
|
||||
|
||||
it('ueberspringt eine Kopfzeile, wenn die zweite Spalte keine Zahl ist', () => {
|
||||
const r = parseKontenCsv(csv('Name;Gegenkonto;Konto\nKaffee;2010;4000'), null);
|
||||
expect(r.accounts).toEqual([{ name: 'Kaffee', gegenkonto: 2010, erloeskonto: 4000 }]);
|
||||
expect(r.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it('nimmt fuer eine fehlende dritte Spalte das Standard-Erloeskonto', () => {
|
||||
const r = parseKontenCsv(csv('Kaffee;2010'), 4711);
|
||||
expect(r.accounts[0].erloeskonto).toBe(4711);
|
||||
});
|
||||
|
||||
it('meldet missingErloeskonto, wenn das Standard-Erloeskonto leer ist', () => {
|
||||
const r = parseKontenCsv(csv('Kaffee;2010'), null);
|
||||
expect(r.errors).toEqual([expect.objectContaining({ line: 1, code: 'missingErloeskonto' })]);
|
||||
});
|
||||
|
||||
it('meldet ungueltige Zahlen und leere Namen mit Zeilennummer', () => {
|
||||
const r = parseKontenCsv(csv('ok;1;2\n;5;6\nx;abc;6\ny;5;-1\nz;0;6'), null);
|
||||
expect(r.errors.map((e) => [e.line, e.code])).toEqual([
|
||||
[2, 'nameEmpty'],
|
||||
[3, 'gegenkontoInvalid'],
|
||||
[4, 'erloeskontoInvalid'],
|
||||
[5, 'gegenkontoInvalid'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('meldet doppelte Namen', () => {
|
||||
const r = parseKontenCsv(csv('Kaffee;1;2\nKaffee;3;4'), null);
|
||||
expect(r.errors).toEqual([expect.objectContaining({ line: 2, code: 'duplicateName' })]);
|
||||
});
|
||||
|
||||
it('meldet zu lange Namen', () => {
|
||||
const r = parseKontenCsv(csv(`${'x'.repeat(121)};1;2`), null);
|
||||
expect(r.errors[0].code).toBe('nameTooLong');
|
||||
});
|
||||
|
||||
it('erlaubt Semikolons im Namen', () => {
|
||||
const r = parseKontenCsv(csv('Tee; gruen;2010;4000'), null);
|
||||
expect(r.accounts[0]).toEqual({ name: 'Tee; gruen', gegenkonto: 2010, erloeskonto: 4000 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateKontenCsv', () => {
|
||||
it('beginnt mit BOM, nutzt Semikolon und CRLF', () => {
|
||||
const out = generateKontenCsv([
|
||||
{ name: 'Käse', gegenkonto: 2010, erloeskonto: 4000 },
|
||||
{ name: 'Tee', gegenkonto: 2011, erloeskonto: 4001 },
|
||||
]);
|
||||
expect(out.startsWith('')).toBe(true);
|
||||
expect(out.slice(1)).toBe('Käse;2010;4000\r\nTee;2011;4001\r\n');
|
||||
});
|
||||
|
||||
it.each([
|
||||
'=SUMME(A1)',
|
||||
'+1',
|
||||
'-5 % Aktion',
|
||||
'@cmd',
|
||||
])('schuetzt %j mit einem Apostroph', (name) => {
|
||||
const out = generateKontenCsv([{ name, gegenkonto: 1, erloeskonto: 2 }]);
|
||||
expect(out.slice(1).startsWith(`'${name};`)).toBe(true);
|
||||
});
|
||||
|
||||
it('Export und Import ergeben dieselbe Liste (Rundlauf)', () => {
|
||||
const accounts = [
|
||||
{ name: '=1+1', gegenkonto: 2010, erloeskonto: 4000 },
|
||||
{ name: '-5 % Aktion', gegenkonto: 2011, erloeskonto: 4000 },
|
||||
{ name: 'Käse', gegenkonto: 2012, erloeskonto: 4001 },
|
||||
];
|
||||
const back = parseKontenCsv(Buffer.from(generateKontenCsv(accounts), 'utf8'), null);
|
||||
expect(back.errors).toEqual([]);
|
||||
expect(back.accounts).toEqual(accounts);
|
||||
});
|
||||
|
||||
it('leere Liste ergibt nur das BOM', () => {
|
||||
expect(generateKontenCsv([])).toBe('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user