Einnahmen-Überschuss-Rechnung mit ELSTER, DATEV und Mandantenfähigkeit

Gewinnermittlung nach §4 Abs. 3 EStG für Einzelunternehmen und Freiberufler.
Node/Fastify mit SQLite im Backend, React/Vite im Frontend; die Anwendung
läuft lokal ohne Cloud-Anbindung.

Fachlicher Kern ist eine Zeitachse der Besteuerungsform: das umsatzsteuerliche
Regime wird je Buchung aus dem Leistungsdatum aufgelöst, statt aus einer
globalen Ja/Nein-Flagge. Dadurch ist auch ein unterjähriger Wechsel von der
Kleinunternehmerregelung zur Regelbesteuerung abbildbar, wie ihn §19 Abs. 1
Satz 3 UStG seit 2025 beim Überschreiten der 100.000-EUR-Grenze vorsieht.

Enthalten:
- Buchungserfassung nach dem Zu-/Abflussprinzip mit Storno statt Löschung
- Anlagevermögen mit AfA (linear, GWG, Sammelposten); die Aktivierung
  entscheidet das Konto, die Methode der Betrag
- Anlage EÜR, BWA im Aufbau der Form 01, Umsatzsteuer-Voranmeldung
- ELSTER-Anbindung über ERiC mit validierendem Simulationsmodus
- DATEV-Buchungsstapel (EXTF 700) und GoBD-Datenträgerüberlassung (Z3)
- Kontenrahmen SKR03 und SKR04, je Mandant beim Anlegen wählbar
- Mandantenfähigkeit mit einer SQLite-Datei je Mandant; Testmandanten sind
  von der Übermittlung ans Finanzamt ausgeschlossen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-03 21:45:52 +02:00
co-authored by Claude Opus 5
commit bf371c8118
59 changed files with 12913 additions and 0 deletions
+377
View File
@@ -0,0 +1,377 @@
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { resolve, dirname, isAbsolute } from 'node:path';
import { createRequire } from 'node:module';
import { alsLatin1 } from './ustvaXml.js';
import { datenVerzeichnis, projektWurzel } from '../pfade.js';
const require = createRequire(import.meta.url);
/**
* Anbindung an ERiC (ELSTER Rich Client).
*
* ERiC ist die einzige zugelassene Schnittstelle, um Steueranmeldungen an die
* Finanzverwaltung zu übermitteln. Die Bibliothek ist proprietär, darf nicht
* weitergegeben werden und muss nach Registrierung im ELSTER-Entwicklerportal
* selbst heruntergeladen werden. Sie wird über Umgebungsvariablen eingebunden:
*
* ERIC_LIB_PATH Pfad zu libericapi.so / ericapi.dll / libericapi.dylib
* ERIC_PLUGIN_PATH Pfad zum Verzeichnis "plugins2"
* ERIC_LOG_PATH Verzeichnis für eric.log (Standard: ./data/eric-log)
* ELSTER_ZERTIFIKAT Pfad zur .pfx-Zertifikatsdatei (ELSTER-Zertifikat)
* ELSTER_PIN PIN zum Zertifikat
* ELSTER_HERSTELLER_ID bei ELSTER registrierte Hersteller-ID
* ELSTER_TESTMERKER z.B. 700000004 für Testübermittlungen ohne Wirkung
*
* Ist ERiC nicht eingerichtet, arbeitet die Anwendung im Simulationsmodus:
* das XML wird erzeugt und geprüft, aber ausdrücklich nicht übermittelt.
*/
const ERIC_OK = 0;
const FLAG_VALIDIERE = 1 << 1; // 2
const FLAG_SENDE = 1 << 2; // 4
const FLAG_DRUCKE = 1 << 5; // 32
export interface EricAntwort {
erfolg: boolean;
simulation: boolean;
rueckgabecode: number;
transferticket: string | null;
/** Von ERiC gelieferte Validierungs-/Serverantwort */
protokoll: string;
fehler: string[];
hinweise: string[];
pdfPfad?: string;
}
export interface SendeOptionen {
/** Nur prüfen, nicht übermitteln */
nurValidieren: boolean;
/** Zusätzlich ein Übertragungsprotokoll als PDF erzeugen */
pdfPfad?: string;
}
interface EricBindings {
Initialise(plugin: string, log: string): number;
Beende(): number;
BearbeiteVorgang(
daten: Buffer,
datenartVersion: string,
flags: number,
druck: unknown,
crypto: unknown,
rueckgabe: unknown,
serverantwort: unknown,
): number;
PufferErzeugen(): unknown;
PufferInhalt(h: unknown): string;
PufferFreigeben(h: unknown): number;
ZertifikatOeffnen(pfad: string): { handle: number; pinSupport: number };
ZertifikatSchliessen(handle: number): number;
FehlerText(code: number): string;
}
let bindings: EricBindings | null = null;
let ladeFehler: string | null = null;
let initialisiert = false;
/** Relative Angaben aus der .env beziehen sich auf die Projektwurzel. */
function ausWurzel(pfad: string): string {
return isAbsolute(pfad) ? pfad : resolve(projektWurzel, pfad);
}
function logVerzeichnis(): string {
const konfiguriert = process.env.ERIC_LOG_PATH;
const p = konfiguriert ? ausWurzel(konfiguriert) : resolve(datenVerzeichnis, 'eric-log');
mkdirSync(p, { recursive: true });
return p;
}
function ladeBindings(): EricBindings | null {
if (bindings) return bindings;
if (ladeFehler) return null;
const libPfad = process.env.ERIC_LIB_PATH;
if (!libPfad || !existsSync(libPfad)) {
ladeFehler = libPfad
? `ERIC_LIB_PATH zeigt auf "${libPfad}", dort liegt keine Datei.`
: 'ERIC_LIB_PATH ist nicht gesetzt ERiC ist nicht eingebunden.';
return null;
}
try {
// koffi ist eine optionale Abhängigkeit; ohne ERiC wird sie nicht gebraucht.
const koffi = require('koffi') as typeof import('koffi');
const lib = koffi.load(libPfad);
const druckParameter = koffi.struct('eric_druck_parameter_t', {
version: 'uint32',
vorschau: 'uint32',
ersteSeite: 'uint32',
duplexDruck: 'uint32',
pdfName: 'const char*',
fussText: 'const char*',
});
const cryptoParameter = koffi.struct('eric_verschluesselungs_parameter_t', {
version: 'uint32',
zertifikatHandle: 'uint32',
pin: 'const char*',
abrufCode: 'const char*',
});
const fnInitialise = lib.func('int EricInitialise(const char* pluginPfad, const char* logPfad)');
const fnBeende = lib.func('int EricBeende()');
const fnPufferErzeugen = lib.func('void* EricRueckgabepufferErzeugen()');
const fnPufferInhalt = lib.func('const char* EricRueckgabepufferInhalt(void* handle)');
const fnPufferFreigeben = lib.func('int EricRueckgabepufferFreigeben(void* handle)');
const fnBearbeite = lib.func(
'int EricBearbeiteVorgang(const char* daten, const char* datenartVersion, uint32 flags, ' +
'const eric_druck_parameter_t* druck, ' +
'const eric_verschluesselungs_parameter_t* crypto, void* transferHandle, ' +
'void* rueckgabe, void* serverantwort)',
);
const fnZertifikat = lib.func(
'int EricGetHandleToCertificate(_Out_ uint32* handle, _Out_ uint32* pinSupport, const char* pfad)',
);
const fnZertifikatZu = lib.func('int EricCloseHandleToCertificate(uint32 handle)');
const fnFehlertext = lib.func('int EricHoleFehlerText(int fehlercode, void* puffer)');
void druckParameter;
void cryptoParameter;
bindings = {
Initialise: (plugin, log) => fnInitialise(plugin, log) as number,
Beende: () => fnBeende() as number,
BearbeiteVorgang: (daten, dv, flags, druck, crypto, rueckgabe, serverantwort) =>
fnBearbeite(daten, dv, flags, druck, crypto, null, rueckgabe, serverantwort) as number,
PufferErzeugen: () => fnPufferErzeugen(),
PufferInhalt: (h) => (fnPufferInhalt(h) as string) ?? '',
PufferFreigeben: (h) => fnPufferFreigeben(h) as number,
ZertifikatOeffnen: (pfad) => {
const handle: number[] = [0];
const pinSupport: number[] = [0];
const rc = fnZertifikat(handle, pinSupport, pfad) as number;
if (rc !== ERIC_OK) throw new Error(`Zertifikat konnte nicht geöffnet werden (ERiC ${rc}).`);
return { handle: handle[0]!, pinSupport: pinSupport[0]! };
},
ZertifikatSchliessen: (h) => fnZertifikatZu(h) as number,
FehlerText: (code) => {
const puffer = fnPufferErzeugen();
try {
fnFehlertext(code, puffer);
return (fnPufferInhalt(puffer) as string) ?? '';
} finally {
fnPufferFreigeben(puffer);
}
},
};
return bindings;
} catch (e) {
ladeFehler = `ERiC konnte nicht geladen werden: ${(e as Error).message}`;
return null;
}
}
function initialisiere(b: EricBindings): void {
if (initialisiert) return;
const plugin = process.env.ERIC_PLUGIN_PATH ?? '';
const rc = b.Initialise(plugin, logVerzeichnis());
if (rc !== ERIC_OK) {
throw new Error(`EricInitialise fehlgeschlagen (Code ${rc}). ERIC_PLUGIN_PATH prüfen.`);
}
initialisiert = true;
process.once('exit', () => {
try {
b.Beende();
} catch {
/* beim Herunterfahren ignorieren */
}
});
}
export function ericStatus(): {
verfuegbar: boolean;
meldung: string;
zertifikat: boolean;
herstellerId: boolean;
testmodus: boolean;
} {
const b = ladeBindings();
const zertPfad = process.env.ELSTER_ZERTIFIKAT;
return {
verfuegbar: b !== null,
meldung: b ? `ERiC geladen von ${process.env.ERIC_LIB_PATH}` : (ladeFehler ?? 'unbekannt'),
zertifikat: !!zertPfad && existsSync(zertPfad),
herstellerId: !!process.env.ELSTER_HERSTELLER_ID,
testmodus: !!process.env.ELSTER_TESTMERKER,
};
}
export async function uebermittle(
xml: string,
datenartVersion: string,
opt: SendeOptionen,
): Promise<EricAntwort> {
const b = ladeBindings();
if (!b) return simuliere(xml, opt, ladeFehler ?? 'ERiC nicht verfügbar');
initialisiere(b);
const zertPfad = process.env.ELSTER_ZERTIFIKAT;
const pin = process.env.ELSTER_PIN ?? '';
if (!opt.nurValidieren && (!zertPfad || !existsSync(zertPfad))) {
return {
erfolg: false,
simulation: false,
rueckgabecode: -1,
transferticket: null,
protokoll: '',
fehler: [
'Für die Übermittlung wird ein ELSTER-Zertifikat benötigt. ' +
'Bitte ELSTER_ZERTIFIKAT auf die .pfx-Datei und ELSTER_PIN setzen.',
],
hinweise: [],
};
}
let zertHandle = 0;
const rueckgabe = b.PufferErzeugen();
const serverantwort = b.PufferErzeugen();
try {
let flags = FLAG_VALIDIERE;
if (!opt.nurValidieren) flags |= FLAG_SENDE;
if (opt.pdfPfad) flags |= FLAG_DRUCKE;
let crypto: unknown = null;
if (!opt.nurValidieren && zertPfad) {
const z = b.ZertifikatOeffnen(zertPfad);
zertHandle = z.handle;
crypto = { version: 2, zertifikatHandle: zertHandle, pin, abrufCode: null };
}
let druck: unknown = null;
if (opt.pdfPfad) {
mkdirSync(dirname(opt.pdfPfad), { recursive: true });
druck = {
version: 4,
vorschau: opt.nurValidieren ? 1 : 0,
ersteSeite: 0,
duplexDruck: 0,
pdfName: opt.pdfPfad,
fussText: 'Erstellt mit open-eur-webapp',
};
}
const rc = b.BearbeiteVorgang(
alsLatin1(xml),
datenartVersion,
flags,
druck,
crypto,
rueckgabe,
serverantwort,
);
const protokoll = b.PufferInhalt(rueckgabe);
const antwort = b.PufferInhalt(serverantwort);
const gesamt = [protokoll, antwort].filter(Boolean).join('\n');
return {
erfolg: rc === ERIC_OK,
simulation: false,
rueckgabecode: rc,
transferticket: transferticketAus(antwort),
protokoll: gesamt,
fehler: rc === ERIC_OK ? [] : [`ERiC-Code ${rc}: ${sicherFehlertext(b, rc)}`, ...fehlerAus(gesamt)],
hinweise: hinweiseAus(gesamt),
pdfPfad: opt.pdfPfad,
};
} catch (e) {
return {
erfolg: false,
simulation: false,
rueckgabecode: -1,
transferticket: null,
protokoll: '',
fehler: [(e as Error).message],
hinweise: [],
};
} finally {
if (zertHandle) {
try {
b.ZertifikatSchliessen(zertHandle);
} catch {
/* ignorieren */
}
}
b.PufferFreigeben(rueckgabe);
b.PufferFreigeben(serverantwort);
}
}
function sicherFehlertext(b: EricBindings, code: number): string {
try {
return b.FehlerText(code) || 'kein Klartext verfügbar';
} catch {
return 'kein Klartext verfügbar';
}
}
function transferticketAus(xml: string): string | null {
return /<TransferTicket>([^<]+)<\/TransferTicket>/.exec(xml)?.[1] ?? null;
}
function fehlerAus(xml: string): string[] {
return [...xml.matchAll(/<Text>([^<]+)<\/Text>/g)].map((m) => m[1]!).slice(0, 20);
}
function hinweiseAus(xml: string): string[] {
return [...xml.matchAll(/<Hinweis>([\s\S]*?)<\/Hinweis>/g)].map((m) => m[1]!.trim()).slice(0, 20);
}
/**
* Simulationsmodus ohne ERiC: prüft das erzeugte XML auf Wohlgeformtheit und
* offensichtliche Lücken und legt es zur Ansicht ab. Es findet ausdrücklich
* keine Übermittlung an die Finanzverwaltung statt.
*/
function simuliere(xml: string, opt: SendeOptionen, grund: string): EricAntwort {
const fehler: string[] = [];
const hinweise = [
`ERiC ist nicht eingebunden (${grund}).`,
'Es wurde NICHTS an das Finanzamt übermittelt. Die Anmeldung wurde lediglich erzeugt und geprüft.',
'Zum echten Versand: ERiC im ELSTER-Entwicklerportal herunterladen und ERIC_LIB_PATH, ' +
'ERIC_PLUGIN_PATH, ELSTER_ZERTIFIKAT, ELSTER_PIN sowie ELSTER_HERSTELLER_ID setzen.',
];
// einfache Wohlgeformtheitsprüfung: alle Tags müssen sich schließen
const stack: string[] = [];
for (const m of xml.matchAll(/<(\/?)([A-Za-z][\w.-]*)[^>]*?(\/?)>/g)) {
const [, schliessend, name, selbst] = m;
if (selbst === '/' || name === 'xml') continue;
if (schliessend === '/') {
if (stack.pop() !== name) fehler.push(`XML nicht wohlgeformt bei </${name}>.`);
} else {
stack.push(name!);
}
}
if (stack.length > 0) fehler.push(`Nicht geschlossene Elemente: ${stack.join(', ')}.`);
if (!/<Steuernummer>\d{13}<\/Steuernummer>/.test(xml)) {
fehler.push('Die Steuernummer liegt nicht im 13-stelligen Bundesformat vor.');
}
if (!/<Kz83>/.test(xml)) fehler.push('Kennzahl 83 (Zahllast) fehlt.');
const ablage = resolve(datenVerzeichnis, 'elster-ausgang');
mkdirSync(ablage, { recursive: true });
const datei = resolve(ablage, `ustva-simulation-${Date.now()}.xml`);
writeFileSync(datei, alsLatin1(xml));
return {
erfolg: fehler.length === 0,
simulation: true,
rueckgabecode: fehler.length === 0 ? 0 : -1,
transferticket: null,
protokoll: `Simulationsmodus. XML abgelegt unter ${datei}.`,
fehler,
hinweise: opt.nurValidieren ? hinweise.slice(0, 1).concat(hinweise[2]!) : hinweise,
};
}
+115
View File
@@ -0,0 +1,115 @@
/**
* Umrechnung der auf dem Steuerbescheid gedruckten Landes-Steuernummer in das
* 13-stellige bundeseinheitliche Format, das ELSTER erwartet.
*
* Aufbau: <2- bis 3-stelliges Landespräfix><Finanzamtsnummer><0><Bezirk><Unterscheidungsnr.><Prüfziffer>
*/
export type Bundesland =
| 'BW' | 'BY' | 'BE' | 'BB' | 'HB' | 'HH' | 'HE' | 'MV'
| 'NI' | 'NW' | 'RP' | 'SL' | 'SN' | 'ST' | 'SH' | 'TH';
export const BUNDESLAENDER: { code: Bundesland; name: string }[] = [
{ code: 'BW', name: 'Baden-Württemberg' },
{ code: 'BY', name: 'Bayern' },
{ code: 'BE', name: 'Berlin' },
{ code: 'BB', name: 'Brandenburg' },
{ code: 'HB', name: 'Bremen' },
{ code: 'HH', name: 'Hamburg' },
{ code: 'HE', name: 'Hessen' },
{ code: 'MV', name: 'Mecklenburg-Vorpommern' },
{ code: 'NI', name: 'Niedersachsen' },
{ code: 'NW', name: 'Nordrhein-Westfalen' },
{ code: 'RP', name: 'Rheinland-Pfalz' },
{ code: 'SL', name: 'Saarland' },
{ code: 'SN', name: 'Sachsen' },
{ code: 'ST', name: 'Sachsen-Anhalt' },
{ code: 'SH', name: 'Schleswig-Holstein' },
{ code: 'TH', name: 'Thüringen' },
];
type Regel =
/** 10-stellige Landesnummer FFBBBUUUUP -> <praefix>FF0BBBUUUUP */
| { typ: 'kurz'; praefix: string }
/** 11-stellige Landesnummer mit führender Füllziffer -> wie 'kurz' nach Abschneiden */
| { typ: 'fuellziffer'; praefix: string }
/** Bayern: FFFBBBUUUUP -> 9FFF0BBBUUUUP */
| { typ: 'bayern' }
/** NRW: FFFBBBBUUUP -> 5FFF0BBBBUUUP */
| { typ: 'nrw' };
const REGELN: Record<Bundesland, Regel> = {
BW: { typ: 'kurz', praefix: '28' },
BY: { typ: 'bayern' },
BE: { typ: 'kurz', praefix: '11' },
BB: { typ: 'fuellziffer', praefix: '30' },
HB: { typ: 'kurz', praefix: '24' },
HH: { typ: 'kurz', praefix: '22' },
HE: { typ: 'fuellziffer', praefix: '26' },
MV: { typ: 'fuellziffer', praefix: '40' },
NI: { typ: 'kurz', praefix: '23' },
NW: { typ: 'nrw' },
RP: { typ: 'kurz', praefix: '27' },
SL: { typ: 'kurz', praefix: '10' },
SN: { typ: 'fuellziffer', praefix: '32' },
ST: { typ: 'fuellziffer', praefix: '31' },
SH: { typ: 'kurz', praefix: '21' },
TH: { typ: 'fuellziffer', praefix: '41' },
};
export class SteuernummerFehler extends Error {}
export function nurZiffern(s: string): string {
return s.replace(/\D/g, '');
}
export function bundeseinheitlich(steuernummer: string, land: Bundesland): string {
const z = nurZiffern(steuernummer);
if (!z) throw new SteuernummerFehler('Es ist keine Steuernummer hinterlegt.');
const regel = REGELN[land];
let ergebnis: string;
if (regel.typ === 'bayern') {
if (z.length !== 11) {
throw new SteuernummerFehler(
`Bayerische Steuernummern haben 11 Ziffern (Format 123/456/78901), erhalten: ${z.length}.`,
);
}
ergebnis = `9${z.slice(0, 3)}0${z.slice(3)}`;
} else if (regel.typ === 'nrw') {
if (z.length !== 11) {
throw new SteuernummerFehler(
`Nordrhein-westfälische Steuernummern haben 11 Ziffern (Format 123/4567/8901), erhalten: ${z.length}.`,
);
}
ergebnis = `5${z.slice(0, 3)}0${z.slice(3)}`;
} else {
let kurz = z;
if (regel.typ === 'fuellziffer') {
if (z.length === 11) kurz = z.slice(1);
else if (z.length !== 10) {
throw new SteuernummerFehler(
`Steuernummer für ${land} muss 10 oder 11 Ziffern haben, erhalten: ${z.length}.`,
);
}
} else if (z.length !== 10) {
throw new SteuernummerFehler(
`Steuernummer für ${land} muss 10 Ziffern haben (Format 12/345/67890), erhalten: ${z.length}.`,
);
}
ergebnis = `${regel.praefix}${kurz.slice(0, 2)}0${kurz.slice(2)}`;
}
if (ergebnis.length !== 13) {
throw new SteuernummerFehler(
`Umrechnung ergab ${ergebnis.length} statt 13 Stellen bitte Bundesland und Steuernummer prüfen.`,
);
}
return ergebnis;
}
/** Die ersten vier Stellen sind die Bundesfinanzamtsnummer ELSTER-Empfänger-ID. */
export function finanzamtNummer(bundesSteuernummer: string): string {
return bundesSteuernummer.slice(0, 4);
}
+145
View File
@@ -0,0 +1,145 @@
import type { Mandant } from '../domain/typen.js';
import type { UstvaErgebnis } from '../domain/ustva.js';
import { kennzahlenFuerElster } from '../domain/ustva.js';
import { bundeseinheitlich, finanzamtNummer, type Bundesland } from './steuernummer.js';
export const HERSTELLER = {
name: 'open-eur-webapp',
version: '0.1.0',
/**
* ERiC verlangt eine bei ELSTER registrierte Hersteller-ID. Ohne gültige ID
* lehnt ERiC die Übermittlung ab hier über die Umgebung konfigurierbar.
*/
herstellerId: process.env.ELSTER_HERSTELLER_ID ?? '00000',
};
/**
* Datenart-Version für EricBearbeiteVorgang, z.B. "UStVA_2026".
* Muss zu der im installierten ERiC enthaltenen Version passen.
*/
export function datenartVersion(jahr: number): string {
return `UStVA_${jahr}`;
}
function esc(s: string): string {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
export interface XmlOptionen {
berichtigt?: boolean;
/** Kz 26: Verrechnung des Erstattungsbetrags erwünscht */
verrechnungErwuenscht?: boolean;
/** Kz 29: Belege werden gesondert eingereicht */
belegnachreichung?: boolean;
/** Kz 22/23: Einzugsermächtigung soll ausnahmsweise nicht genutzt werden */
keinLastschriftEinzug?: boolean;
erstellungsdatum?: string; // YYYYMMDD
}
export interface XmlErgebnis {
xml: string;
bundesSteuernummer: string;
datenartVersion: string;
warnungen: string[];
}
/**
* Erzeugt den ELSTER-Datenteil für eine Umsatzsteuer-Voranmeldung.
* Den TransferHeader (Verschlüsselung, Signatur, Routing) ergänzt ERiC selbst
* übergeben wird nur dieses Dokument.
*/
export function ustvaXml(
ergebnis: UstvaErgebnis,
mandant: Mandant,
opt: XmlOptionen = {},
): XmlErgebnis {
const warnungen: string[] = [];
const stnr = bundeseinheitlich(mandant.steuernummer, mandant.bundesland as Bundesland);
const jahr = ergebnis.zeitraum.jahr;
const kz = kennzahlenFuerElster(ergebnis.kennzahlen);
if (HERSTELLER.herstellerId === '00000') {
warnungen.push(
'Es ist keine ELSTER-Hersteller-ID gesetzt (ELSTER_HERSTELLER_ID). ' +
'ERiC weist die Übermittlung ohne registrierte ID zurück.',
);
}
const erstellung =
opt.erstellungsdatum ?? new Date().toISOString().slice(0, 10).replace(/-/g, '');
const kzZeilen: string[] = [];
if (opt.berichtigt) kzZeilen.push(' <Kz10>1</Kz10>');
if (opt.belegnachreichung) kzZeilen.push(' <Kz22>1</Kz22>');
if (opt.keinLastschriftEinzug) kzZeilen.push(' <Kz26>1</Kz26>');
if (opt.verrechnungErwuenscht) kzZeilen.push(' <Kz29>1</Kz29>');
// Kennzahlen in aufsteigender numerischer Reihenfolge so erwartet es das Schema.
for (const k of Object.keys(kz).sort((a, b) => Number(a) - Number(b))) {
kzZeilen.push(` <Kz${k}>${kz[k]}</Kz${k}>`);
}
const xml = `<?xml version="1.0" encoding="ISO-8859-15"?>
<Elster xmlns="http://www.elster.de/elsterxml/schema/v11">
<TransferHeader version="11">
<Verfahren>ElsterAnmeldung</Verfahren>
<DatenArt>UStVA</DatenArt>
<Vorgang>send-Auth</Vorgang>
<Testmerker>${process.env.ELSTER_TESTMERKER ?? ''}</Testmerker>
<HerstellerID>${esc(HERSTELLER.herstellerId)}</HerstellerID>
<DatenLieferant>${esc(mandant.name)}</DatenLieferant>
<Datei>
<Verschluesselung>PKCS#7v1.5</Verschluesselung>
<Kompression>GZIP</Kompression>
<TransportSchluessel/>
<Erstellung><Eric/></Erstellung>
</Datei>
<VersionClient>${esc(HERSTELLER.version)}</VersionClient>
</TransferHeader>
<DatenTeil>
<Nutzdatenblock>
<NutzdatenHeader version="11">
<NutzdatenTicket>1</NutzdatenTicket>
<Empfaenger id="F">${finanzamtNummer(stnr)}</Empfaenger>
<Hersteller>
<ProduktName>${esc(HERSTELLER.name)}</ProduktName>
<ProduktVersion>${esc(HERSTELLER.version)}</ProduktVersion>
</Hersteller>
</NutzdatenHeader>
<Nutzdaten>
<Anmeldungssteuern art="UStVA" version="${jahr}">
<DatenLieferant>
<Name>${esc(mandant.name)}</Name>
<Strasse>${esc(mandant.strasse)}</Strasse>
<PLZ>${esc(mandant.plz)}</PLZ>
<Ort>${esc(mandant.ort)}</Ort>
<Telefon>${esc(mandant.telefon)}</Telefon>
<Email>${esc(mandant.email)}</Email>
</DatenLieferant>
<Erstellungsdatum>${erstellung}</Erstellungsdatum>
<Steuerfall>
<Umsatzsteuervoranmeldung>
<Jahr>${jahr}</Jahr>
<Zeitraum>${ergebnis.zeitraum.code}</Zeitraum>
<Steuernummer>${stnr}</Steuernummer>
${kzZeilen.join('\n')}
</Umsatzsteuervoranmeldung>
</Steuerfall>
</Anmeldungssteuern>
</Nutzdaten>
</Nutzdatenblock>
</DatenTeil>
</Elster>
`;
return { xml, bundesSteuernummer: stnr, datenartVersion: datenartVersion(jahr), warnungen };
}
/** ELSTER erwartet ISO-8859-15; Umlaute müssen als latin1-Bytes übergeben werden. */
export function alsLatin1(xml: string): Buffer {
return Buffer.from(xml, 'latin1');
}