From 4f8e9565749707551e09b383ed0183c98e98800c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:15:25 +0000 Subject: [PATCH] Rechnungen: Anzahlung und Schlussrechnung bauen (Minuszeilen, Entwurfsschutz, Selbstheilung) Co-Authored-By: Claude Sonnet 5.5 --- server/services/auftraege/rechnungsbau.js | 112 ++++++++++++++-- test/auftraegeRechnungsbau.test.js | 155 ++++++++++++++++++++++ 2 files changed, 256 insertions(+), 11 deletions(-) diff --git a/server/services/auftraege/rechnungsbau.js b/server/services/auftraege/rechnungsbau.js index d6afe5d..080f0c6 100644 --- a/server/services/auftraege/rechnungsbau.js +++ b/server/services/auftraege/rechnungsbau.js @@ -9,18 +9,20 @@ import { getStripe } from '../stripeBilling.js' import { withLocks } from '../keyedLock.js' import { AuftragError } from './fehler.js' import { - RECHNUNGS_ARTEN, LIMITS, VEREINBARTE_STATUS, STUNDENSATZ_STANDARD_CENTS, SCHALTER_AUS_MELDUNG, sperreAuftrag, + RECHNUNGS_ARTEN, AB_RECHNUNGS_ARTEN, LIMITS, VEREINBARTE_STATUS, STUNDENSATZ_STANDARD_CENTS, SCHALTER_AUS_MELDUNG, sperreAuftrag, } from './konstanten.js' import { - festePositionen, zusatzPositionen, pruefePositionen, kuerzen, auftragDatum, aufwandPositionen, leistungszeitraumText, SCHLUSS_GRENZEN, + festePositionen, zusatzPositionen, pruefePositionen, kuerzen, auftragDatum, aufwandPositionen, leistungszeitraumText, + anzahlungPosition, schlussPositionen, SCHLUSS_GRENZEN, } from './positionen.js' import { auftraegeAktiv } from './schalter.js' import { getAuftrag, listAuftraege, updateAuftrag } from './auftragStore.js' -import { updateProjektRechnung } from './projektRechnungenStore.js' +import { listProjektRechnungen, updateProjektRechnung } from './projektRechnungenStore.js' import { ladeArbeitsblaetter, ladeArbeitsblaetterNachIds, abrechenbar, abgerechneteArbeitsblaetter, offeneArbeitsblaetter, zeitquelleFuerProjekt, } from './arbeitsblaetter.js' -import { formatDe } from '../finance/dates.js' +import { berlinDate, formatDe } from '../finance/dates.js' +import { euroDe } from '../finance/format.js' // Appwrite-Dokument-ID const KENNUNG = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/ @@ -40,18 +42,25 @@ export async function ladeFestenKunden(customerId) { return kunde } -/** Projekt des Kunden; fremde und unbekannte Projekte sind nicht zu unterscheiden (404). */ -async function ladeProjekt(kundenId, projektId) { - if (typeof projektId !== 'string' || !projektId) throw new AuftragError(400, 'Für diese Rechnungsart ist ein Projekt nötig.') - if (!KENNUNG.test(projektId)) throw new AuftragError(404, 'Projekt nicht gefunden.') +/** + * Projekt des Kunden; fremde und unbekannte Projekte sind nicht zu unterscheiden (404). Mit weich = true liefert jeder + * Fehler stattdessen null (nur Lesefehler von Appwrite werden weitergereicht). + */ +async function ladeProjekt(kundenId, projektId, { weich = false } = {}) { + const fehler = (status, meldung) => { + if (weich) return null + throw new AuftragError(status, meldung) + } + if (typeof projektId !== 'string' || !projektId) return fehler(400, 'Für diese Rechnungsart ist ein Projekt nötig.') + if (!KENNUNG.test(projektId)) return fehler(404, 'Projekt nicht gefunden.') let projekt try { projekt = await getDocument(config.collections.websiteProjects, projektId) } catch (err) { - if (err.status === 404) throw new AuftragError(404, 'Projekt nicht gefunden.') + if (err.status === 404) return fehler(404, 'Projekt nicht gefunden.') throw err } - if (projekt.customerId !== kundenId) throw new AuftragError(404, 'Projekt nicht gefunden.') + if (projekt.customerId !== kundenId) return fehler(404, 'Projekt nicht gefunden.') return { id: projekt.$id, name: projekt.projectName || projekt.subdomain || 'Projekt', ticketId: projekt.ticketId || '' } } @@ -175,6 +184,7 @@ export async function baueRechnung(eingabe) { const art = eingabe.art ?? 'frei' if (typeof art !== 'string' || !RECHNUNGS_ARTEN.includes(art)) throw new AuftragError(400, 'Unbekannte Rechnungsart.') const kunde = await ladeFestenKunden(eingabe.customerId) + if (AB_RECHNUNGS_ARTEN.includes(art) && !auftraegeAktiv(kunde)) throw new AuftragError(400, SCHALTER_AUS_MELDUNG) const leistungszeitraum = pruefeLeistungszeitraum(eingabe.leistungszeitraum) const basis = { kunde, art, leistungszeitraum, createdBy: eingabe.createdBy } if (art === 'frei') return schliesseAb({ ...basis, positionen: festePositionen(eingabe.items) }) @@ -183,7 +193,7 @@ export async function baueRechnung(eingabe) { return schliesseAb({ ...basis, projekt, positionen: festePositionen(eingabe.items) }) } if (art === 'aufwand') return baueAufwand(basis, eingabe) - throw new AuftragError(501, 'Rechnungsart noch nicht umgesetzt.') + return baueAbRechnung(basis, eingabe) } const MAX_BLAETTER = 400 @@ -282,3 +292,83 @@ async function baueAufwand(basis, eingabe) { arbeitsblattIds: blaetter.map((b) => b.id), }) } + +const FINALISIERT = ['open', 'paid', 'uncollectible'] + +/** + * Fruehere Projektrechnungen einer AB (aktive Datensaetze) mit dem Stand bei Stripe. Selbstheilung: eine im Dashboard + * stornierte oder geloeschte Rechnung wird freigegeben (bei einer Schlussrechnung geht die AB zurueck) und uebersprungen. + */ +async function ladeFruehere(auftragId) { + const ergebnis = [] + for (const doc of await listProjektRechnungen({ auftragId })) { + let roh + try { + roh = await getStripe().invoices.retrieve(doc.invoiceId) + } catch (err) { + if (!fehltBeiStripe(err)) throw err + roh = null + } + if (!roh || roh.status === 'void') { + await buche('Rechnung freigeben', () => gibProjektRechnungFrei(doc.invoiceId)) + continue + } + const nummer = roh.number || '' + if (FINALISIERT.includes(roh.status) && nummer && !doc.rechnungsnummer) { + await buche('Rechnungsnummer nachtragen', () => updateProjektRechnung(doc.invoiceId, { rechnungsnummer: nummer })) + } + ergebnis.push({ + invoiceId: doc.invoiceId, + art: doc.art, + status: roh.status, + rechnungsnummer: nummer, + datum: berlinDate(new Date((roh.status_transitions?.finalized_at ?? roh.created) * 1000)), + betragCents: roh.total ?? 0, + }) + } + return ergebnis +} + +/** Anzahlung und Schlussrechnung zu einer Festpreis-AB. */ +async function baueAbRechnung(basis, eingabe) { + const { kunde, art } = basis + const { auftragId } = eingabe + if (typeof auftragId !== 'string' || !auftragId) throw new AuftragError(400, 'Anzahlung und Schlussrechnung brauchen eine Auftragsbestätigung.') + let auftrag = await ladeAuftrag(kunde.$id, auftragId) + if (auftrag.art !== 'festpreis') throw new AuftragError(400, 'Anzahlung und Schlussrechnung gehen nur mit einer Festpreis-Auftragsbestätigung.') + if (auftrag.status === 'abgerechnet') { + await ladeFruehere(auftrag.id) // gibt eine im Dashboard stornierte Schlussrechnung frei und setzt die AB zurueck + auftrag = await ladeAuftrag(kunde.$id, auftragId) + } + if (!['versendet', 'bestaetigt'].includes(auftrag.status)) throw new AuftragError(400, 'Die Auftragsbestätigung muss versendet oder bestätigt sein.') + + let projekt = null + if (typeof eingabe.projektId === 'string' && eingabe.projektId) { + projekt = await ladeProjekt(kunde.$id, eingabe.projektId) + if (auftrag.projektId && auftrag.projektId !== projekt.id) throw new AuftragError(400, 'Der Auftrag gehört nicht zu diesem Projekt.') + } else if (auftrag.projektId) { + projekt = await ladeProjekt(kunde.$id, auftrag.projektId, { weich: true }) + } + + const fruehere = await ladeFruehere(auftrag.id) + const entwurf = fruehere.find((f) => f.status === 'draft' && (art === 'schluss' || f.art === 'schluss')) + if (entwurf) { + throw new AuftragError(409, art === 'schluss' + ? `Erst den Entwurf über ${euroDe(entwurf.betragCents)} senden oder löschen.` + : 'Zu dieser Auftragsbestätigung gibt es schon einen Entwurf der Schlussrechnung. Erst den Entwurf senden oder löschen.') + } + const finalisierte = fruehere.filter((f) => FINALISIERT.includes(f.status)) + let positionen + if (art === 'anzahlung') { + const angaben = eingabe.anzahlung ?? {} + if (typeof angaben !== 'object' || Array.isArray(angaben)) throw new AuftragError(400, 'Die Angaben zur Anzahlung müssen ein Objekt sein.') + positionen = [anzahlungPosition(auftrag, angaben, finalisierte.reduce((s, f) => s + f.betragCents, 0))] + } else { + positionen = schlussPositionen( + auftrag, + finalisierte.map(({ invoiceId, rechnungsnummer, datum, betragCents, art: fArt }) => ({ invoiceId, rechnungsnummer, datum, betragCents, art: fArt })), + eingabe.items + ) + } + return schliesseAb({ ...basis, projekt, auftrag, positionen }) +} diff --git a/test/auftraegeRechnungsbau.test.js b/test/auftraegeRechnungsbau.test.js index 10a2437..deb8842 100644 --- a/test/auftraegeRechnungsbau.test.js +++ b/test/auftraegeRechnungsbau.test.js @@ -252,3 +252,158 @@ test('baueRechnung aufwand: Projekt ist Pflicht, fremdes Projekt 404, Projekt oh } assert.equal((await baueRechnung(eingabe({ zeilentexte: null }))).positionen.length, 2) }) + +/** Frühere Rechnung zur AB (Art anzahlung, wenn nicht anders gesagt): Stripe-Rechnung im Nachbau plus aktiver Projektdatensatz. */ +const fruehereRechnung = (ab, { id, art = 'anzahlung', status = 'open', ...optionen }) => legeRechnungAn(st, id, { art, status, auftragId: ab.id, ...optionen }) + +test('baueRechnung anzahlung: Position aus dem Zahlungsplan, Zusatzfelder Projekt und Auftrag, Metadaten, Projektdatensatz', async () => { + const ab = await legeAuftragAn() + const e = await baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, createdBy: 'kenso@webklar.com' }) + assert.deepEqual(e.positionen.map((p) => [p.schluessel, p.beschreibung, p.betragCents]), [ + ['anzahlung', 'Anzahlung 50 % gemäß Auftragsbestätigung AB-2026-0002 vom 06.10.2026 (Website anelia-wolf.com)', 40000], + ]) + assert.equal(e.summeCents, 40000) + assert.deepEqual(e.customFields, [{ name: 'Projekt', value: 'Website anelia-wolf.com' }, { name: 'Auftrag', value: 'AB-2026-0002 vom 06.10.2026' }]) + assert.deepEqual(e.metadata, { + webklarCustomerId: 'k1', quelle: 'woms', erstelltVon: 'kenso@webklar.com', art: 'anzahlung', + projektId: 'p1', projektName: 'Website anelia-wolf.com', auftragId: ab.id, auftragNummer: 'AB-2026-0002', + }) + assert.equal(e.auftrag.id, ab.id) + assert.equal(e.projekt.id, 'p1') + const { erstelltAm, ...rest } = e.projektRechnung + assert.ok(erstelltAm) + assert.deepEqual(rest, { kundenId: 'k1', projektId: 'p1', auftragId: ab.id, art: 'anzahlung', betragCents: 40000, arbeitsblattIds: [], rechnungsnummer: '', aktiv: true, erstelltVon: 'kenso@webklar.com' }) + assert.equal(st.calls.length, 0, 'ohne frühere Rechnungen gibt es nichts bei Stripe zu lesen') +}) + +test('baueRechnung anzahlung: Prozent oder Betrag änderbar; nur finalisierte Rechnungen mindern das Offene', async () => { + const ab = await legeAuftragAn() + const lauf = (anzahlung) => baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id, anzahlung }) + assert.ok((await lauf({ prozent: 25, bezeichnung: 'Teilrechnung' })).positionen[0].beschreibung.startsWith('Teilrechnung 25 % gemäß')) + assert.equal((await lauf({ betragCents: 25000 })).summeCents, 25000) + await fruehereRechnung(ab, { id: 'in_f1', status: 'open', total: 40000 }) + await fruehereRechnung(ab, { id: 'in_f2', status: 'void', total: 99000, nummer: 'WEBKLAR-0008' }) // storniert: zählt nicht + await fruehereRechnung(ab, { id: 'in_f3', status: 'draft', total: 5000 }) // Entwurf einer Anzahlung: zählt nicht + assert.equal((await lauf(undefined)).summeCents, 40000) + assert.equal((await getProjektRechnung('in_f2')).aktiv, false, 'die im Dashboard stornierte Rechnung wurde freigegeben') + await wirft(lauf({ betragCents: 40001 }), 400, `Die Anzahlung darf höchstens 400,00${NBSP}€ betragen (Summe der Auftragsbestätigung minus bereits berechnet).`) + await fruehereRechnung(ab, { id: 'in_f4', status: 'paid', total: 40000, nummer: 'WEBKLAR-0009' }) + await wirft(lauf(undefined), 409, 'Nichts mehr offen.') + for (const kaputt of ['viel', 5, ['x']]) await wirft(lauf(kaputt), 400, 'Die Angaben zur Anzahlung müssen ein Objekt sein.') +}) + +test('baueRechnung anzahlung: ein Entwurf der Schlussrechnung zur AB blockiert weitere Anzahlungen (409)', async () => { + const ab = await legeAuftragAn() + await fruehereRechnung(ab, { id: 'in_s1', status: 'draft', art: 'schluss', total: 40000 }) + await wirft( + baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: ab.id }), + 409, + 'Zu dieser Auftragsbestätigung gibt es schon einen Entwurf der Schlussrechnung. Erst den Entwurf senden oder löschen.' + ) +}) + +test('baueRechnung anzahlung/schluss: Voraussetzungen (Schalter, Auftrag, Festpreis, Status, Kunde, Projekt)', async () => { + const ab = await legeAuftragAn() + for (const art of ['anzahlung', 'schluss']) { + await wirft(baueRechnung({ customerId: 'k1', art }), 400, 'Anzahlung und Schlussrechnung brauchen eine Auftragsbestätigung.') + await wirft(baueRechnung({ customerId: 'k1', art, auftragId: 'gibtEsNicht' }), 404, 'Auftrag nicht gefunden.') + await wirft(baueRechnung({ customerId: 'k2', art, auftragId: ab.id }), 404, 'Auftrag nicht gefunden.') + await wirft(baueRechnung({ customerId: 'k1', art, auftragId: ab.id, projektId: 'p3' }), 400, 'Der Auftrag gehört nicht zu diesem Projekt.') + config.auftraege.mode = 'off' + try { + await wirft(baueRechnung({ customerId: 'k1', art, auftragId: ab.id }), 400, SCHALTER_AUS) + await wirft(baueRechnung({ customerId: 'k1', art }), 400, SCHALTER_AUS) + } finally { + config.auftraege.mode = 'on' + } + } + const aufwand = await legeAuftragAn({ art: 'aufwand', positionen: [], summeCents: null, stundensatzCents: 10000 }, { nummer: 'AB-2026-0006' }) + await wirft(baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: aufwand.id }), 400, 'Anzahlung und Schlussrechnung gehen nur mit einer Festpreis-Auftragsbestätigung.') + for (const [i, status] of ['entwurf', 'storniert', 'ersetzt', 'widerrufen', 'abgerechnet'].entries()) { + const x = await legeAuftragAn({}, { nummer: `AB-2026-010${i}`, status }) + for (const art of ['anzahlung', 'schluss']) await wirft(baueRechnung({ customerId: 'k1', art, auftragId: x.id }), 400, 'Die Auftragsbestätigung muss versendet oder bestätigt sein.') + } + const bestaetigt = await legeAuftragAn({}, { nummer: 'AB-2026-0007', status: 'bestaetigt', bestaetigtAm: '2026-10-07T09:00:00.000Z' }) + assert.equal((await baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: bestaetigt.id })).summeCents, 40000) +}) + +test('baueRechnung schluss: AB-Positionen, Zusatz und eine Minuszeile je finalisierter, nicht stornierter früherer Rechnung', async () => { + const ab = await legeAuftragAn() + await fruehereRechnung(ab, { id: 'in_f1', status: 'open', total: 40000, nummer: 'WEBKLAR-0007', finalisiert: '2026-10-08T10:00:00.000Z' }) + await fruehereRechnung(ab, { id: 'in_f2', status: 'void', total: 20000, nummer: 'WEBKLAR-0008' }) + const e = await baueRechnung({ customerId: 'k1', art: 'schluss', auftragId: ab.id, items: [{ description: 'Domain-Registrierung', quantity: 1, unitAmountCents: 1500 }] }) + assert.deepEqual(e.positionen.map((p) => [p.schluessel, p.betragCents]), [['ab-1', 60000], ['ab-2', 20000], ['zusatz-1', 1500], ['abzug-in_f1', -40000]]) + assert.equal(e.positionen.at(-1).beschreibung, 'abzüglich Anzahlung, Rechnung WEBKLAR-0007 vom 08.10.2026') + assert.equal(e.summeCents, 41500) + assert.deepEqual(e.customFields.map((f) => f.name), ['Projekt', 'Auftrag']) + assert.equal(e.projektRechnung.art, 'schluss') + assert.equal(e.projektRechnung.betragCents, 41500) + assert.equal(e.projektRechnung.auftragId, ab.id) +}) + +test('baueRechnung schluss: offener Entwurf zur AB blockiert (409), Rechnungen fremder ABs und bei Stripe gelöschte zählen nicht, nichts mehr offen -> 409', async () => { + const ab = await legeAuftragAn() + const andere = await legeAuftragAn({}, { nummer: 'AB-2026-0003' }) + await fruehereRechnung(andere, { id: 'in_g1', status: 'draft', total: 11111 }) + await fruehereRechnung(andere, { id: 'in_g2', status: 'paid', total: 22222, nummer: 'WEBKLAR-0005' }) + const lauf = () => baueRechnung({ customerId: 'k1', art: 'schluss', auftragId: ab.id }) + assert.equal((await lauf()).summeCents, 80000) + await fruehereRechnung(ab, { id: 'in_h1', status: 'draft', total: 40000 }) + await wirft(lauf(), 409, `Erst den Entwurf über 400,00${NBSP}€ senden oder löschen.`) + await updateProjektRechnung('in_h1', { aktiv: false }) + assert.equal((await lauf()).summeCents, 80000) + await fruehereRechnung(ab, { id: 'in_h2', status: 'paid', total: 80000, nummer: 'WEBKLAR-0008' }) + await wirft(lauf(), 409, 'Nichts mehr offen.') + await updateProjektRechnung('in_h2', { aktiv: false }) + // aktiver Projektdatensatz, aber die Rechnung gibt es bei Stripe nicht mehr (im Dashboard gelöscht): zählt nicht + await fruehereRechnung(ab, { id: 'in_weg', ohneStripe: true, total: 5000, docNummer: 'X' }) + const e = await lauf() + assert.equal(e.summeCents, 80000) + assert.equal(e.positionen.length, 2) + assert.equal((await getProjektRechnung('in_weg')).aktiv, false, 'der Datensatz der gelöschten Rechnung wurde freigegeben') +}) + +test('baueRechnung anzahlung: AB ohne Projekt oder mit gelöschtem Projekt -> Zusatzfeld Projekt zeigt den AB-Titel; ein genanntes fremdes Projekt 404', async () => { + const ohne = await legeAuftragAn({ projektId: '', titel: 'Logo-Entwurf' }, { nummer: 'AB-2026-0012' }) + const e = await baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: ohne.id }) + assert.equal(e.projekt, null) + assert.deepEqual(e.customFields[0], { name: 'Projekt', value: 'Logo-Entwurf' }) + assert.equal(e.metadata.projektId, undefined) + assert.equal(e.projektRechnung.projektId, '') + const weg = await legeAuftragAn({ projektId: 'pWeg' }, { nummer: 'AB-2026-0013' }) + const w = await baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: weg.id }) + assert.equal(w.projekt, null) + assert.deepEqual([w.metadata.projektId, w.projektRechnung.projektId, w.customFields[0].value], ['pWeg', 'pWeg', 'Website anelia-wolf.com']) + assert.equal((await baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: ohne.id, projektId: 'p1' })).projekt.id, 'p1') + await wirft(baueRechnung({ customerId: 'k1', art: 'anzahlung', auftragId: ohne.id, projektId: 'p2' }), 404, 'Projekt nicht gefunden.') +}) + +test('baueRechnung schluss: eine im Dashboard stornierte Schlussrechnung gibt die AB wieder frei (Selbstheilung), eine offene hält sie', async () => { + const ab = await legeAuftragAn({}, { status: 'abgerechnet', bestaetigtAm: '2026-10-07T09:00:00.000Z' }) + await fruehereRechnung(ab, { id: 'in_sl1', art: 'schluss', status: 'open', total: 80000 }) + await wirft(baueRechnung({ customerId: 'k1', art: 'schluss', auftragId: ab.id }), 400, 'Die Auftragsbestätigung muss versendet oder bestätigt sein.') + assert.equal((await getAuftrag(ab.id)).status, 'abgerechnet') + st.invoices.set('in_sl1', { ...st.invoices.get('in_sl1'), status: 'void' }) // im Dashboard storniert + const e = await baueRechnung({ customerId: 'k1', art: 'schluss', auftragId: ab.id }) + assert.equal(e.summeCents, 80000) + assert.equal((await getAuftrag(ab.id)).status, 'bestaetigt') + assert.equal((await getProjektRechnung('in_sl1')).aktiv, false) +}) + +test('baueRechnung schluss: bis zu 40 Zeilen (20 AB-Positionen, 10 Nachträge, 10 Abzüge); mehr Abzüge oder Nachträge -> 400', async () => { + const positionen = Array.from({ length: 20 }, (_, i) => ({ beschreibung: `Pos ${i + 1}`, menge: 1, einzelpreisCents: 1000 })) + const ab = await legeAuftragAn({ positionen, summeCents: 20000 }, { nummer: 'AB-2026-0014' }) + const nachtraege = (n) => Array.from({ length: n }, (_, i) => ({ description: `Nachtrag ${i + 1}`, quantity: 1, unitAmountCents: 500 })) + const lauf = (items) => baueRechnung({ customerId: 'k1', art: 'schluss', auftragId: ab.id, items }) + const abzug = (i) => fruehereRechnung(ab, { id: `in_z${String(i).padStart(2, '0')}`, status: 'paid', total: 100, nummer: `WK-${String(i).padStart(4, '0')}` }) + assert.equal((await lauf(undefined)).positionen.length, 20) + await abzug(1) + const eins = await lauf(undefined) + assert.deepEqual([eins.positionen.length, eins.summeCents], [21, 19900], '20 AB-Positionen und ein Abzug sind gültig') + for (let i = 2; i <= 10; i += 1) await abzug(i) + const voll = await lauf(nachtraege(10)) + assert.deepEqual([voll.positionen.length, voll.summeCents, voll.projektRechnung.betragCents], [40, 24000, 24000]) + await wirft(lauf(nachtraege(11)), 400, 'Eine Schlussrechnung erlaubt höchstens 10 Zusatzpositionen.') + await abzug(11) + await wirft(lauf(undefined), 400, 'Die Schlussrechnung hätte 11 Abzüge für frühere Rechnungen; erlaubt sind höchstens 10.') +})