feat(accounting): Pflichtangaben der E-Rechnung vor dem Fertigstellen durchsetzen
Fehlende Pflichtangaben (Firmendaten, Steuer-ID, Kundenanschrift, IBAN, § 13b-USt-Id, Lastschriftmandat) erscheinen im Editor als blockierende Fehler und verhindern Vorschau und Fertigstellung. Serienrechnungslauf, MCP-Finalisierung und PDF-Erzeugung prüfen serverseitig vor der Nummernvergabe bzw. Ablage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
import { PDFDocument, StandardFonts } from "pdf-lib"
|
||||
import { buildZugferdInvoiceXml, countryCode, unitCode, ZugferdInvoiceContext } from "../src/modules/einvoice/buildZugferdInvoice"
|
||||
import { buildZugferdInvoiceXml, checkZugferdRequirements, countryCode, unitCode, ZugferdInvoiceContext } from "../src/modules/einvoice/buildZugferdInvoice"
|
||||
import { embedZugferdXml } from "../src/modules/einvoice/embedZugferd"
|
||||
import { detectElectronicInvoice } from "../src/modules/einvoice/detectElectronicInvoice"
|
||||
import { parseElectronicInvoice } from "../src/modules/einvoice/parseElectronicInvoice"
|
||||
@@ -32,6 +32,7 @@ const context = (overrides: Partial<ZugferdInvoiceContext> = {}): ZugferdInvoice
|
||||
const invoice = (overrides: any = {}) => ({
|
||||
id: 1,
|
||||
type: "invoices",
|
||||
customer: 5,
|
||||
documentNumber: "RE-2026-0042",
|
||||
documentDate: "2026-11-04",
|
||||
deliveryDateType: "Lieferdatum",
|
||||
@@ -55,9 +56,9 @@ const invoice = (overrides: any = {}) => ({
|
||||
const tag = (xml: string, name: string) => [...xml.matchAll(new RegExp(`<ram:${name}(?:\\s[^>]*)?>([^<]*)</ram:${name}>`, "g"))].map((match) => match[1])
|
||||
|
||||
test("erzeugt eine EN-16931-Rechnung mit konsistenten Summen", () => {
|
||||
const { xml, warnings, totals } = buildZugferdInvoiceXml(invoice(), context())
|
||||
const { xml, issues, totals } = buildZugferdInvoiceXml(invoice(), context())
|
||||
|
||||
assert.deepEqual(warnings, [])
|
||||
assert.deepEqual(issues, [])
|
||||
// 23,45 + 81,00 + 20,00 - 5,00 (optionale Position ausgeschlossen)
|
||||
assert.equal(totals.lineNet, 119.45)
|
||||
// 19 %: 99,45 -> 18,90; 7 %: 20,00 -> 1,40
|
||||
@@ -142,18 +143,15 @@ test("§ 13b und Kleinunternehmer verwenden die passenden Steuerkategorien", ()
|
||||
const smallBusiness = buildZugferdInvoiceXml(invoice({ taxType: "19 UStG", rows: [{ pos: "1", mode: "free", text: "Beratung", quantity: 1, unit: 1, price: 100, taxPercent: 0 }] }), context())
|
||||
assert.match(smallBusiness.xml, /<ram:ExemptionReason>Kein Ausweis von Umsatzsteuer, da Kleinunternehmer gemäß § 19 UStG<\/ram:ExemptionReason>/)
|
||||
assert.match(smallBusiness.xml, /<ram:CategoryCode>E<\/ram:CategoryCode>/)
|
||||
|
||||
const missingVat = buildZugferdInvoiceXml(invoice({ taxType: "13b UStG" }), context({ customer: { name: "Privat", infoData: { city: "X" } } }))
|
||||
assert.ok(missingVat.warnings.some((warning) => warning.includes("USt-IdNr. des Kunden")))
|
||||
})
|
||||
|
||||
test("Leistungszeitraum, Lastschrift und Skonto werden strukturiert übertragen", () => {
|
||||
const { xml, warnings } = buildZugferdInvoiceXml(invoice({
|
||||
const { xml, issues } = buildZugferdInvoiceXml(invoice({
|
||||
deliveryDateType: "Leistungszeitraum", deliveryDate: "2026-10-01", deliveryDateEnd: "2026-10-31",
|
||||
payment_type: "direct-debit", cashDiscountPercent: 2, cashDiscountDays: 7,
|
||||
}), context({ sepaMandate: { reference: "MANDAT-1" } }))
|
||||
|
||||
assert.deepEqual(warnings, [])
|
||||
assert.deepEqual(issues, [])
|
||||
assert.match(xml, /<ram:BillingSpecifiedPeriod><ram:StartDateTime><udt:DateTimeString format="102">20261001/)
|
||||
assert.match(xml, /<ram:CreditorReferenceID>DE98ZZZ09999999999<\/ram:CreditorReferenceID>/)
|
||||
assert.match(xml, /<ram:TypeCode>59<\/ram:TypeCode>/)
|
||||
@@ -170,3 +168,42 @@ test("ordnet Länder und Einheiten den Codelisten zu", () => {
|
||||
assert.equal(unitCode({ short: "m²" }), "MTK")
|
||||
assert.equal(unitCode({ short: "unbekannt" }), "C62")
|
||||
})
|
||||
|
||||
const breakingMessages = (document: any, ctx: ZugferdInvoiceContext, requireNumber = false) =>
|
||||
checkZugferdRequirements(document, ctx, { requireNumber }).filter((issue) => issue.type === "breaking").map((issue) => issue.message)
|
||||
|
||||
test("Pflichtangaben: vollständiger Beleg hat keine blockierenden Fehler", () => {
|
||||
assert.deepEqual(breakingMessages(invoice(), context(), true), [])
|
||||
assert.deepEqual(breakingMessages(invoice({ documentNumber: null }), context()), [])
|
||||
assert.deepEqual(breakingMessages(invoice({ documentNumber: null }), context(), true), ["E-Rechnung: Die Rechnungsnummer fehlt"])
|
||||
})
|
||||
|
||||
test("Pflichtangaben: fehlende Firmendaten, Kundenanschrift und IBAN blockieren", () => {
|
||||
const ctx = context({
|
||||
tenant: { businessInfo: { name: "Muster GmbH", street: "", zip: "12345", city: "Musterstadt" } },
|
||||
})
|
||||
const messages = breakingMessages(invoice({ address: { street: "Kundenweg 5", zip: "", city: "Kundenstadt" } }), ctx)
|
||||
|
||||
assert.ok(messages.some((message) => message.includes("Firmenanschrift")))
|
||||
assert.ok(messages.some((message) => message.includes("weder eine USt-IdNr. noch eine Steuernummer")))
|
||||
assert.ok(messages.some((message) => message.includes("Anschrift des Kunden")))
|
||||
assert.ok(messages.some((message) => message.includes("keine IBAN")))
|
||||
// Ein Bankkonto mit IBAN genügt anstelle der IBAN in den Firmendaten
|
||||
const withAccount = context({ bankAccounts: [{ iban: "DE02120300000000202051" }] })
|
||||
withAccount.tenant.businessInfo.iban = ""
|
||||
assert.deepEqual(breakingMessages(invoice(), withAccount), [])
|
||||
})
|
||||
|
||||
test("Pflichtangaben: § 13b und Lastschrift verlangen ihre Zusatzangaben", () => {
|
||||
const reverseCharge = breakingMessages(invoice({ taxType: "13b UStG" }), context({ customer: { name: "Bau AG", infoData: {} } }))
|
||||
assert.deepEqual(reverseCharge, ["E-Rechnung: Für § 13b UStG muss beim Kunden eine USt-Id hinterlegt sein"])
|
||||
|
||||
const directDebit = breakingMessages(invoice({ payment_type: "direct-debit" }), context({ tenant: { ...context().tenant, creditorId: "" } }))
|
||||
assert.deepEqual(directDebit, [
|
||||
"E-Rechnung: Für SEPA-Lastschrift ist in den Firmendaten keine Gläubiger-ID hinterlegt",
|
||||
"E-Rechnung: Für SEPA-Lastschrift ist kein Mandat mit Mandatsreferenz ausgewählt",
|
||||
])
|
||||
|
||||
const noDate = checkZugferdRequirements(invoice({ deliveryDateType: "Kein Lieferdatum anzeigen" }), context())
|
||||
assert.deepEqual(noDate.map((issue) => issue.type), ["info"])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user