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>
116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
/**
|
||
* 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);
|
||
}
|