Skip to content

Commit 207eb7e

Browse files
fix(ausgangsrechnung): Rechnungsnummern pro Jahr zählen — keine Doppelnummer bei Jahres-Rücksprung
H2 (high, §14 Abs.4 Nr.4 UStG / GoBD): Der Nummernkreis hielt EINEN globalen Zähler {schema, naechste, jahr} und setzte ihn bei jedem Jahreswechsel auf 1 zurück. Rückdatierung ins Vorjahr + Rücksprung ins schon-begonnene Jahr → Reset → bereits vergebene Nummer erneut. Reproduziert: [2026-01,2026-02,2025-12,2026-03,2026-04] → RE-2026-0001/0002 DOPPELT. Datenmodell auf Pro-Jahr-Zähler {zaehler:{<jahr>:<naechste>}} umgestellt: - vergebeNummer/naechsteNummer zählen je Kalenderjahr unabhängig; Schemata ohne {JAHR} teilen einen durchlaufenden Kreis (Schlüssel '*'). - migriereNummern: self-migrating + idempotent; rettet auch manuell gesetzte Startwerte ohne `jahr` aufs laufende Jahr (Altzustand des UI-Zweitbefunds), statt sie zu verwerfen. - UI „Nächste Nummer" bezieht sich eindeutig aufs laufende Jahr; persistiert in zaehler[key] statt blankem naechste (behebt den latenten Zweitbefund, dass das UI nie `jahr` setzte). Schema ohne {NR}-Platzhalter wird abgelehnt. Verifiziert: Repro-Sequenz jetzt duplikatfrei (5 paarweise verschiedene Nummern); Migration {jahr:2026,naechste:5}→RE-2026-0005; 307 Tests grün (UTC/Berlin/LA). Adversarisch refute-reviewt (general-purpose) — Befund „Datenverlust bei naechste-ohne-jahr" gefunden + 1:1 behoben. Design: .planning/H2-...-DESIGN.md. Finaler Diff wird beim gated Release-Vet erneut gegengeprüft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HmxogiBV6uLrvjcui4cC2D
1 parent a1736ba commit 207eb7e

5 files changed

Lines changed: 237 additions & 18 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# H2 — Doppelte Rechnungsnummern bei Jahres-Rücksprung: Datenmodell-Redesign
2+
3+
_Pfenni, 2026-06-21. R22-kritisch (Datenmodell). Design-vor-Build (Deliberate Mode)._
4+
5+
## Problem (reproduziert)
6+
`public/shared/ausgangsrechnung.js` hält EINEN globalen Zähler:
7+
`rechnungsnummern = { schema, naechste, jahr }`. `vergebeNummer` setzt bei
8+
Jahreswechsel (`rn.jahr !== j`) `naechste = 1` zurück. Bei Rückdatierung ins
9+
Vorjahr + Rücksprung ins schon-begonnene Jahr → Reset → DOPPELNUMMER.
10+
11+
Repro (`node`): Daten `[2026-01,2026-02,2025-12,2026-03,2026-04]`
12+
`RE-2026-0001, RE-2026-0002, RE-2025-0001, RE-2026-0001(!), RE-2026-0002(!)`.
13+
Verletzt **§ 14 Abs. 4 Nr. 4 UStG** (Einmaligkeit) + **GoBD**. Realistischer
14+
Pfad: Jahreswechsel-Nacherfassung (Dezember-Rechnung im Januar nachtragen).
15+
16+
Latenter Zweitbefund: UI (`app.js:4188-4198` „Nächste Nummer") setzt nur
17+
`naechste`, NICHT `jahr` → manuell gesetzter Startwert wird beim nächsten
18+
Jahreswechsel-Reset verschluckt (wirkungslos, sobald `rn.jahr !== aktuellesJahr`).
19+
20+
## Architektur-Constraints (recherchiert)
21+
- Rechnungen liegen am **Abschluss** (`a.ausgangsrechnungen`), über Jahre/
22+
Abschlüsse verteilt. Der Zähler liegt am **Unternehmen** (`un.rechnungsnummern`).
23+
`vergebeNummer(u, datum)` bekommt NUR das Unternehmen, kennt die Rechnungsliste
24+
nicht. → „Nummer aus existierenden Rechnungen ableiten" ist architektonisch
25+
nicht verfügbar UND GoBD-widrig (gelöschte/stornierte Nummer würde wiederverwendet).
26+
Der **persistente Zähler IST die autoritative Quelle** — korrekt, nur global statt
27+
pro Jahr. Fix = Zähler pro Jahr, nichts an der Architektur drehen.
28+
- Dual-Mode: Daten leben im Browser (IndexedDB). Migration muss **self-migrating
29+
beim Lesen** sein (kein Migrations-Script), idempotent, rückwärtskompatibel.
30+
- Konsumenten: `app.js:4133/4193` (UI-Feld), `4590` (`naechsteNummer` Vorschau),
31+
`4668` (`vergebeNummer` Vergabe). Tests `tests/run.js:1648-1680`.
32+
33+
## Entscheidung: Zähler pro Jahr als Map
34+
```js
35+
rechnungsnummern = {
36+
schema: 'RE-{JAHR}-{NR:04}',
37+
zaehler: { "2025": 6, "2026": 3 } // jahr(String) -> NÄCHSTE freie Nr für dieses Jahr
38+
}
39+
```
40+
- `naechsteNummer(u, datum)`: `j = jahrAus(datum); nr = zaehler[j] || 1;` (keine Mutation).
41+
- `vergebeNummer(u, datum)`: `j; nr = zaehler[j] || 1; zaehler[j] = nr + 1;` → nie Reset
42+
eines bereits begonnenen Jahres. Rücksprung ins Vorjahr stellt NUR `zaehler[vorjahr]`
43+
weiter, lässt `zaehler[aktuellesJahr]` unberührt → keine Doppelnummer.
44+
45+
### Migration (self-migrating, idempotent) — in `defaults()` zentralisiert
46+
```js
47+
function migriereNummern(rn) {
48+
if (rn && (!rn.zaehler || typeof rn.zaehler !== 'object')) {
49+
rn.zaehler = {};
50+
// alten globalen Single-Zähler in das richtige Jahr übernehmen
51+
if (rn.jahr > 0 && rn.naechste > 0) rn.zaehler[String(rn.jahr)] = rn.naechste;
52+
}
53+
return rn;
54+
}
55+
```
56+
Greift genau einmal (sobald `zaehler` existiert, no-op). Verlässt sich nur auf
57+
`rn`, kein Listen-Zugriff. Alte Felder `jahr`/`naechste` bleiben als toter
58+
Ballast stehen (NICHT gelöscht — siehe Tradeoff Rückwärtskompat unten).
59+
60+
### UI-Anpassung (app.js Nummernkreis)
61+
„Nächste Nummer" bezieht sich auf das **laufende Kalenderjahr** (`jahrAus(heute)`):
62+
- Vorbelegung lesen: `zaehler[lj] || 1`.
63+
- Speichern: `zaehler[lj] = n` (statt blankes `naechste`). Label präzisieren:
64+
„Nächste Nummer (laufendes Jahr <LJ>)". Behebt den latenten Zweitbefund.
65+
66+
## Verworfene Alternativen
67+
1. **Nummer aus `a.ausgangsrechnungen` max+1 ableiten.** Verworfen: Architektur
68+
(Liste nicht in `vergebeNummer` verfügbar, über Abschlüsse verteilt) + GoBD
69+
(Wiederverwendung gelöschter Nummern). Der persistente Zähler existiert genau
70+
deshalb.
71+
2. **Single-Zähler behalten, Reset nur bei `j > rn.jahr` (monoton vorwärts).**
72+
Verworfen: bei Rückdatierung ins Vorjahr würde der Vorjahres-Zähler vom
73+
aktuellen Jahr „mitgezählt" → falsche/springende Vorjahresnummern; und ein
74+
zweites Vorjahr (2024 nach 2026) bekäme keinen sauberen eigenen Kreis. Pro-Jahr
75+
ist das einzig saubere Modell für jahresbasierte Schemata.
76+
3. **Legacy-Felder bei Migration löschen.** Verworfen (vorerst): eine alte App-
77+
Version aus dem SW-Cache, die dieselben IDB-Daten lädt, würde ohne `zaehler`
78+
wieder auf `jahr`/`naechste` zurückfallen. Sie stehenzulassen schadet nicht;
79+
`zaehler` ist eindeutig die neue autoritative Quelle. (Defensive Spiegelung der
80+
Legacy-Felder beim Vergeben erwogen, aber als Überkomplexität verworfen — nach
81+
einem Reload ist die alte App ohnehin weg.)
82+
83+
## Risiken / Kanten (zu prüfen)
84+
- Schemata OHNE `{JAHR}` (z. B. fortlaufend über Jahre, `RE-{NR:05}`). Dann ist
85+
„pro Jahr" semantisch falsch — der User WILL einen durchlaufenden Kreis. → Muss
86+
behandelt werden: wenn das Schema kein `{JAHR}` enthält, EINEN gemeinsamen Kreis
87+
führen (z. B. `zaehler['*']`), nicht pro Kalenderjahr. **Kernfrage für Codex.**
88+
- `jahrAus` Default = aktuelles Jahr bei leerem/ungültigem Datum — unverändert ok.
89+
- Idempotenz der Migration bei wiederholtem `defaults()`-Aufruf — durch `zaehler`-
90+
Existenzprüfung gegeben.
91+
92+
## Verifikation (Oracle)
93+
- Repro-Sequenz `[2026-01,2026-02,2025-12,2026-03,2026-04]` → 5 PAARWEISE
94+
VERSCHIEDENE Nummern (`RE-2026-0001/0002, RE-2025-0001, RE-2026-0003, RE-2026-0004`).
95+
- Migration: altes `{jahr:2026,naechste:5}` → erste Vergabe 2026 = `RE-2026-0005`.
96+
- Bestehende Tests (`run.js:1648ff`) angepasst + neue Regressionstests für
97+
Rücksprung + Migration + schema-ohne-JAHR. `node tests/run.js` grün.

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@ nachvollziehbar, welcher Programmstand einen Abschluss erzeugt hat.
4545
4180 (Durchschnittssätze § 24 UStG, Land-/Forstwirtschaft). Jetzt korrekt
4646
getrennt: innergem. Lieferung → 4125 (§ 4 Nr. 1b UStG), sonstige steuerfreie
4747
Umsätze → 4150 (§ 4 Nr. 2-7 UStG).
48+
- **Doppelte Rechnungsnummern bei Jahres-Rücksprung:** Der Rechnungsnummernkreis
49+
führte nur einen globalen Zähler und setzte ihn bei jedem Jahreswechsel auf 1
50+
zurück. Wurde nach Rechnungen des laufenden Jahres eine Rechnung ins Vorjahr
51+
rückdatiert und danach wieder im laufenden Jahr fortgefahren, vergab der Zähler
52+
bereits benutzte Nummern erneut (Verstoß gegen § 14 Abs. 4 Nr. 4 UStG /
53+
GoBD). Jetzt führt jedes Kalenderjahr einen eigenen, lückenlosen Kreis
54+
(Schemata ohne `{JAHR}` einen durchlaufenden) — das Datenmodell migriert
55+
bestehende Zählerstände self-migrating. Das Stammdaten-Feld „Nächste Nummer"
56+
bezieht sich nun eindeutig auf das laufende Jahr; ein Nummernschema ohne
57+
`{NR}`-Platzhalter wird beim Speichern abgelehnt.
4858

4959
### Sicherheit / Robustheit
5060
- **E-Bilanz-Validierung gehärtet** (`lib/validate.js`): doppelter Callback im

public/app.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4122,15 +4122,29 @@ function renderKunden(m) {
41224122
html += '</div>';
41234123

41244124
/* --- Rechnungsnummernkreis -------------------------------------------- */
4125+
/* Pro-Jahr-Zähler: das Feld „Nächste Nummer" bezieht sich auf das laufende
4126+
* Kalenderjahr (bzw. den durchlaufenden Kreis bei Schemata ohne {JAHR}). */
4127+
var nrLj = new Date().getFullYear();
4128+
var nrObj = (u && u.rechnungsnummern) || {};
4129+
var nrSchema = nrObj.schema || 'RE-{JAHR}-{NR:04}';
4130+
var nrProJahr = /\{JAHR\}/.test(nrSchema);
4131+
var nrKey = nrProJahr ? String(nrLj) : '*';
4132+
var nrNaechste = (nrObj.zaehler && nrObj.zaehler[nrKey] > 0) ? nrObj.zaehler[nrKey]
4133+
: ((nrObj.jahr === nrLj && nrObj.naechste > 0) ? nrObj.naechste : 1);
4134+
var nrLabel = nrProJahr ? ('Nächste Nummer (Jahr ' + nrLj + ')')
4135+
: 'Nächste Nummer (durchlaufend)';
41254136
html += '<div class="karte"><h2>Rechnungsnummernkreis</h2>' +
41264137
'<div class="karte-hint">§ 14 Abs. 4 Nr. 4 UStG verlangt eine ' +
41274138
'einmalig vergebene Rechnungsnummer. Platzhalter: ' +
41284139
'<code>{JAHR}</code> für das Rechnungsjahr, <code>{NR:04}</code> für ' +
4129-
'die nächste Nummer mit vier Stellen führender Null. Beim Jahreswechsel ' +
4130-
'wird der Zähler automatisch zurückgesetzt.</div>' +
4140+
'die nächste Nummer mit vier Stellen führender Null. Mit <code>{JAHR}</code> ' +
4141+
'führt jedes Kalenderjahr einen eigenen, lückenlosen Kreis — auch bei ' +
4142+
'nachträglich rückdatierten Rechnungen entsteht keine Doppelnummer.</div>' +
41314143
'<div class="gitter g2">' +
41324144
fv('rechnungsnummern.schema', 'Nummernschema', 'z. B. RE-{JAHR}-{NR:04}') +
4133-
fv('rechnungsnummern.naechste', 'Nächste Nummer', 'einmalig manuell anpassbar, z. B. nach Migration', 'number') +
4145+
feldWrap(nrLabel, 'einmalig manuell anpassbar, z. B. nach Migration',
4146+
'<input data-u="rechnungsnummern.naechste" type="number" min="1" value="' +
4147+
esc(nrNaechste) + '">') +
41344148
'</div>' +
41354149
'<div class="btn-reihe"><button class="btn" id="nrSpeichern">' +
41364150
'Nummernkreis speichern</button></div>' +
@@ -4187,12 +4201,25 @@ function renderKunden(m) {
41874201
/* Nummernkreis speichern */
41884202
m.querySelector('#nrSpeichern').onclick = function () {
41894203
var un = JSON.parse(JSON.stringify(u));
4190-
un.rechnungsnummern = un.rechnungsnummern || {};
4191-
un.rechnungsnummern.schema = m.querySelector('[data-u="rechnungsnummern.schema"]').value.trim()
4204+
/* Altdaten erst self-migrating auf den Pro-Jahr-Zähler heben, damit
4205+
* historische Jahres-Zählerstände nicht durch das Speichern verloren gehen. */
4206+
Ausgangsrechnung.defaults(un);
4207+
var schema = m.querySelector('[data-u="rechnungsnummern.schema"]').value.trim()
41924208
|| 'RE-{JAHR}-{NR:04}';
4209+
/* Ohne {NR}-Platzhalter wäre jede Rechnungsnummer identisch — § 14 Abs. 4
4210+
* Nr. 4 UStG verlangt eine einmalig vergebene Nummer. Schema ablehnen. */
4211+
if (!/\{NR(?::\d+)?\}/.test(schema)) {
4212+
alert('Das Nummernschema muss den Platzhalter {NR} (z. B. {NR:04}) für die '
4213+
+ 'fortlaufende Nummer enthalten — sonst wäre keine Rechnungsnummer eindeutig.');
4214+
return;
4215+
}
4216+
un.rechnungsnummern.schema = schema;
41934217
var n = parseInt(m.querySelector('[data-u="rechnungsnummern.naechste"]').value, 10);
41944218
if (!(n > 0)) n = 1;
4195-
un.rechnungsnummern.naechste = n;
4219+
/* „Nächste Nummer" bezieht sich auf das laufende Jahr (bzw. den
4220+
* durchlaufenden Kreis bei Schemata ohne {JAHR}). */
4221+
var key = /\{JAHR\}/.test(schema) ? String(new Date().getFullYear()) : '*';
4222+
un.rechnungsnummern.zaehler[key] = n;
41964223
Store.speichereUnternehmen(un).then(function (g) {
41974224
if (g && !g.fehler) { S.unternehmen = g; hinweisToast('Nummernkreis gespeichert.'); }
41984225
});

public/shared/ausgangsrechnung.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,41 @@
5656
}
5757
function cent(n) { return Math.round((num(n) + Number.EPSILON) * 100) / 100; }
5858

59+
/* Schlüssel im Pro-Jahr-Zähler. Schemata MIT {JAHR} bekommen je Kalenderjahr
60+
* einen eigenen, lückenlosen Kreis; Schemata OHNE {JAHR} (durchlaufende
61+
* Nummerierung) teilen sich den gemeinsamen Schlüssel '*'. */
62+
function zaehlerKey(schema, datum) {
63+
return /\{JAHR\}/.test(String(schema || 'RE-{JAHR}-{NR:04}'))
64+
? String(jahrAus(datum)) : '*';
65+
}
66+
/* Migriert das alte Single-Zähler-Modell { jahr, naechste } self-migrating
67+
* (idempotent) auf den Pro-Jahr-Zähler { zaehler: { <jahr>: <naechste> } }.
68+
* Behebt H2: ein globaler Zähler wurde bei Rücksprung ins schon-begonnene
69+
* Jahr fälschlich auf 1 zurückgesetzt → Doppelnummer (§ 14 Abs. 4 Nr. 4 UStG).
70+
* Greift genau einmal — sobald `zaehler` existiert, ist es ein No-Op. */
71+
function migriereNummern(rn) {
72+
if (rn && (!rn.zaehler || typeof rn.zaehler !== 'object')) {
73+
rn.zaehler = {};
74+
/* Alten Single-Zähler übernehmen. Bei {JAHR}-Schema ohne gültiges `jahr`
75+
* (Altzustand des UI-Zweitbefunds: `naechste` wurde ohne `jahr` gesetzt)
76+
* den manuell gesetzten Startwert auf das laufende Kalenderjahr RETTEN,
77+
* statt ihn zu verwerfen — sonst lautloser Verlust + Reset-auf-1 mit
78+
* Doppelnummern-Potenzial (§ 14 Abs. 4 Nr. 4 UStG / GoBD). */
79+
if (rn.naechste > 0) {
80+
var key = /\{JAHR\}/.test(String(rn.schema || 'RE-{JAHR}-{NR:04}'))
81+
? String(rn.jahr > 0 ? rn.jahr : new Date().getFullYear()) : '*';
82+
rn.zaehler[key] = rn.naechste;
83+
}
84+
}
85+
return rn;
86+
}
87+
5988
function defaults(u) {
6089
var d = u || {};
6190
if (!d.kunden) d.kunden = [];
6291
if (!d.rechnungsnummern) d.rechnungsnummern = { schema: 'RE-{JAHR}-{NR:04}',
63-
naechste: 1, jahr: 0 };
92+
zaehler: {} };
93+
migriereNummern(d.rechnungsnummern);
6494
if (!d.rechnungsAngaben) d.rechnungsAngaben = {};
6595
return d;
6696
}
@@ -105,19 +135,22 @@
105135
var d = defaults(u);
106136
var rn = d.rechnungsnummern;
107137
var j = jahrAus(datum);
108-
var nr = (rn.jahr === j) ? rn.naechste : 1;
138+
var nr = rn.zaehler[zaehlerKey(rn.schema, datum)] || 1;
109139
return formatNummer(rn.schema, j, nr);
110140
}
111-
/* Tatsächlicher Vergabevorgang: Zähler weiterstellen, neue Nummer
112-
* zurückgeben. Stellt Jahreswechsel um. */
141+
/* Tatsächlicher Vergabevorgang: Zähler des betroffenen Jahres weiterstellen,
142+
* neue Nummer zurückgeben. Jeder Kalenderjahr-Kreis (bzw. der durchlaufende
143+
* Kreis bei Schemata ohne {JAHR}) zählt unabhängig — ein Rücksprung ins
144+
* Vorjahr stellt NUR dessen Zähler weiter und lässt das laufende Jahr
145+
* unberührt (kein fälschlicher Reset → keine Doppelnummer). */
113146
function vergebeNummer(u, datum) {
114147
var d = defaults(u);
115148
var rn = d.rechnungsnummern;
116149
var j = jahrAus(datum);
117-
if (rn.jahr !== j) { rn.jahr = j; rn.naechste = 1; }
118-
var nummer = formatNummer(rn.schema, j, rn.naechste);
119-
rn.naechste = (rn.naechste || 1) + 1;
120-
return nummer;
150+
var key = zaehlerKey(rn.schema, datum);
151+
var nr = rn.zaehler[key] || 1;
152+
rn.zaehler[key] = nr + 1;
153+
return formatNummer(rn.schema, j, nr);
121154
}
122155
/* Wählt das passende Erlös-/USt-Konto für eine Position abhängig von der
123156
* Steuerlogik der Rechnung und vom Steuersatz der Position. */

tests/run.js

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,16 +1650,68 @@ test('Ausgangsrechnung: vergebeNummer ist lückenlos und springt am Jahreswechse
16501650
eq(Ausgangsrechnung.vergebeNummer(u, '2026-05-20'), 'RE-2026-0001', 'erste 2026');
16511651
eq(Ausgangsrechnung.vergebeNummer(u, '2026-06-01'), 'RE-2026-0002', 'zweite 2026');
16521652
eq(Ausgangsrechnung.vergebeNummer(u, '2026-12-31'), 'RE-2026-0003', 'dritte 2026');
1653-
eq(Ausgangsrechnung.vergebeNummer(u, '2027-01-02'), 'RE-2027-0001', 'reset bei Jahreswechsel');
1654-
eq(u.rechnungsnummern.naechste, 2, 'Zähler steht auf 2');
1655-
eq(u.rechnungsnummern.jahr, 2027, 'Jahr aktualisiert');
1653+
eq(Ausgangsrechnung.vergebeNummer(u, '2027-01-02'), 'RE-2027-0001', 'eigener Kreis je Jahr');
1654+
eq(u.rechnungsnummern.zaehler['2026'], 4, 'Zähler 2026 steht auf 4');
1655+
eq(u.rechnungsnummern.zaehler['2027'], 2, 'Zähler 2027 steht auf 2');
1656+
});
1657+
1658+
/* H2-Regression (§ 14 Abs. 4 Nr. 4 UStG): Rückdatierung ins Vorjahr + Rücksprung
1659+
* ins schon-begonnene Jahr darf KEINE Doppelnummer erzeugen. */
1660+
test('Ausgangsrechnung: H2 — Jahres-Rücksprung erzeugt keine Doppelnummer', function () {
1661+
var u = {};
1662+
var seq = ['2026-01-15', '2026-02-15', '2025-12-20', '2026-03-10', '2026-04-05'];
1663+
var nums = seq.map(function (d) { return Ausgangsrechnung.vergebeNummer(u, d); });
1664+
eq(nums.join(','),
1665+
'RE-2026-0001,RE-2026-0002,RE-2025-0001,RE-2026-0003,RE-2026-0004',
1666+
'Vorjahr-Einschub stellt nur den 2025er-Kreis weiter, 2026 läuft lückenlos fort');
1667+
var uniq = nums.filter(function (n, i) { return nums.indexOf(n) === i; });
1668+
eq(uniq.length, nums.length, 'alle vergebenen Nummern sind paarweise verschieden');
1669+
});
1670+
1671+
/* Self-migrating: altes Single-Zähler-Modell { jahr, naechste } übernimmt den
1672+
* Stand in den richtigen Jahres-Kreis. */
1673+
test('Ausgangsrechnung: Migration Single-Zähler -> Pro-Jahr', function () {
1674+
var u = { rechnungsnummern: { schema: 'RE-{JAHR}-{NR:04}', naechste: 5, jahr: 2026 } };
1675+
eq(Ausgangsrechnung.vergebeNummer(u, '2026-05-20'), 'RE-2026-0005', 'setzt bei 5 fort (kein Reset auf 1)');
1676+
eq(Ausgangsrechnung.vergebeNummer(u, '2025-11-01'), 'RE-2025-0001', 'Vorjahr startet eigenen Kreis');
1677+
eq(u.rechnungsnummern.zaehler['2026'], 6, '2026-Kreis fortgeschrieben');
16561678
});
16571679

16581680
test('Ausgangsrechnung: naechsteNummer mutiert nicht (Vorschau)', function () {
16591681
var u = { rechnungsnummern: { schema: 'RE-{JAHR}-{NR:04}', naechste: 5, jahr: 2026 } };
16601682
eq(Ausgangsrechnung.naechsteNummer(u, '2026-05-20'), 'RE-2026-0005', 'Vorschau 5');
16611683
eq(Ausgangsrechnung.naechsteNummer(u, '2026-05-20'), 'RE-2026-0005', 'noch immer 5 (keine Mutation)');
1662-
eq(u.rechnungsnummern.naechste, 5, 'Zähler unverändert');
1684+
eq(u.rechnungsnummern.zaehler['2026'], 5, 'Zähler unverändert');
1685+
});
1686+
1687+
/* Schema ohne {JAHR}: durchlaufende Nummerierung, kein Jahres-Reset. */
1688+
test('Ausgangsrechnung: Schema ohne {JAHR} zählt durchlaufend über Jahresgrenzen', function () {
1689+
var u = { rechnungsnummern: { schema: 'R{NR:05}', zaehler: {} } };
1690+
eq(Ausgangsrechnung.vergebeNummer(u, '2026-12-30'), 'R00001', 'erste');
1691+
eq(Ausgangsrechnung.vergebeNummer(u, '2027-01-02'), 'R00002', 'kein Reset im neuen Jahr');
1692+
eq(Ausgangsrechnung.vergebeNummer(u, '2026-06-01'), 'R00003', 'Rücksprung zählt im selben Kreis weiter');
1693+
eq(u.rechnungsnummern.zaehler['*'], 4, 'gemeinsamer Zähler steht auf 4');
1694+
});
1695+
1696+
/* Migration rettet einen manuell gesetzten Startwert OHNE `jahr` (Altzustand des
1697+
* UI-Zweitbefunds) auf das laufende Jahr, statt ihn zu verwerfen → kein Reset
1698+
* auf 1, keine Doppelnummer (§ 14 Abs. 4 Nr. 4 UStG). */
1699+
test('Ausgangsrechnung: Migration rettet naechste ohne jahr aufs laufende Jahr', function () {
1700+
var lj = String(new Date().getFullYear());
1701+
var u = { rechnungsnummern: { schema: 'RE-{JAHR}-{NR:04}', naechste: 48 } };
1702+
Ausgangsrechnung.defaults(u);
1703+
eq(u.rechnungsnummern.zaehler[lj], 48, 'Startwert 48 ins laufende Jahr gerettet');
1704+
eq(Ausgangsrechnung.vergebeNummer(u, lj + '-03-01'), 'RE-' + lj + '-0048',
1705+
'erste Vergabe setzt bei 48 fort, kein Reset auf 1');
1706+
});
1707+
1708+
/* Migration ist idempotent: wiederholtes defaults() verändert den Zähler nicht. */
1709+
test('Ausgangsrechnung: Migration ist idempotent', function () {
1710+
var u = { rechnungsnummern: { schema: 'RE-{JAHR}-{NR:04}', naechste: 3, jahr: 2026 } };
1711+
Ausgangsrechnung.defaults(u);
1712+
eq(u.rechnungsnummern.zaehler['2026'], 3, 'einmal migriert');
1713+
Ausgangsrechnung.defaults(u);
1714+
eq(u.rechnungsnummern.zaehler['2026'], 3, 'zweiter Aufruf ist No-Op');
16631715
});
16641716

16651717
test('Ausgangsrechnung: eigeneAusUnternehmen — rechnungsAngaben überschreibt Hauptfelder', function () {

0 commit comments

Comments
 (0)