feat(accounting): Skontobedingungen auf Belegen und automatische Skonto-Vorschläge
Ausgangs- und Eingangsbelege erhalten Skontosatz und Frist, mit Vorgaben bei Kunden und Lieferanten. Das PDF ergänzt automatisch einen Skontohinweis (oder nutzt neue Textvorlagen-Variablen); bei Schlussrechnungen gilt der Skonto für die Restforderung. Skontobedingungen werden aus E-Rechnungen (#SKONTO# und ZUGFeRD) sowie von der KI-Vorbereitung übernommen. Die Bankzuordnung erkennt Zahlungen abzüglich Skonto innerhalb der Frist und bietet sie in der Vorschlagsübersicht direkt an. BWA-Erlöse und -Aufwand berücksichtigen gebuchten Skonto im Zeitraum der Zahlung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,3 +114,28 @@ test("MCP allocation input accepts cash discounts only with bank statements", ()
|
||||
amount: 98, cashDiscountAmount: 2, manualBookingDate: "2026-10-01", account: 1, contraAccount: 2,
|
||||
}), /Bankzahlung/)
|
||||
})
|
||||
|
||||
test("builds cash discount terms and the PDF sentence", async () => {
|
||||
const { appendCashDiscountText, buildCashDiscountTerms, formatCashDiscountText, getCashDiscountTemplateContext, parseCashDiscountPaymentTerms } = await import("../src/modules/cash-discount")
|
||||
const terms = buildCashDiscountTerms({ percent: 2, days: 10, documentDate: "2026-11-04", amount: 1190 })
|
||||
assert.deepEqual(terms, { percent: 2, days: 10, dueDate: "2026-11-14", discount: 23.8, payable: 1166.2 })
|
||||
assert.equal(formatCashDiscountText(terms).replace(/ /g, " "),
|
||||
"Bei Zahlung bis zum 14.11.2026 gewähren wir 2 % Skonto (23,80 €), Zahlbetrag 1.166,20 €.")
|
||||
assert.equal(getCashDiscountTemplateContext(terms).skonto_datum, "14.11.2026")
|
||||
assert.match(appendCashDiscountText("Vielen Dank.", "Vielen Dank.", terms), /^Bei Zahlung .*\n\nVielen Dank\.$/)
|
||||
assert.equal(appendCashDiscountText("Eigener Text", "{{skonto_hinweis}}", terms), "Eigener Text")
|
||||
assert.equal(buildCashDiscountTerms({ percent: null, days: 10, documentDate: "2026-11-04", amount: 100 }), null)
|
||||
|
||||
assert.deepEqual(parseCashDiscountPaymentTerms("#SKONTO#TAGE=7#PROZENT=3.00#\n#SKONTO#TAGE=14#PROZENT=2.00#"), { percent: 3, days: 7 })
|
||||
assert.deepEqual(parseCashDiscountPaymentTerms("#SKONTO#TAGE=14#PROZENT=2,5#BASISBETRAG=100.00#"), { percent: 2.5, days: 14 })
|
||||
assert.equal(parseCashDiscountPaymentTerms("Zahlbar ohne Abzug"), null)
|
||||
})
|
||||
|
||||
test("maps explicit AI cash discount data onto incoming invoices", async () => {
|
||||
const { mapGptDataToIncomingInvoice } = await import("../src/modules/cron/prepareIncomingInvoices")
|
||||
const mapped = mapGptDataToIncomingInvoice({ invoice_date: "2026-11-01", cash_discount_percent: 2, cash_discount_days: 10 }, 1)
|
||||
assert.equal(mapped.cashDiscountPercent, 2)
|
||||
assert.equal(mapped.cashDiscountDueDate, "2026-11-11")
|
||||
const ignored = mapGptDataToIncomingInvoice({ invoice_date: "2026-11-01", cash_discount_percent: 25 }, 1)
|
||||
assert.equal(ignored.cashDiscountPercent, undefined)
|
||||
})
|
||||
|
||||
@@ -66,3 +66,27 @@ test("markiert Summenabweichungen als Prüfungsfehler", () => {
|
||||
|
||||
assert.match(parsed.validation.errors.join(" "), /Positionssumme/)
|
||||
})
|
||||
|
||||
test("reads XRechnung cash discount terms from CII payment terms", async () => {
|
||||
const xml = Buffer.from(ciiInvoice.toString().replace(
|
||||
"<ram:SpecifiedTradePaymentTerms>",
|
||||
"<ram:SpecifiedTradePaymentTerms><ram:Description>#SKONTO#TAGE=10#PROZENT=2.00#\n</ram:Description>",
|
||||
))
|
||||
const detection = await detectElectronicInvoice(xml, { name: "rechnung.xml" })
|
||||
const parsed = parseElectronicInvoice(detection!.xml, detection!.syntax)
|
||||
assert.deepEqual(parsed.cashDiscount, { percent: 2, days: 10 })
|
||||
|
||||
const mapped = mapElectronicInvoiceToIncomingInvoice(parsed, 1, 5, "xml", null)
|
||||
assert.equal(mapped.cashDiscountPercent, 2)
|
||||
assert.equal(mapped.cashDiscountDueDate, "2026-07-31")
|
||||
})
|
||||
|
||||
test("reads cash discount terms from UBL payment terms notes", async () => {
|
||||
const xml = Buffer.from(ublInvoice.toString().replace(
|
||||
"<cac:PaymentMeans>",
|
||||
"<cac:PaymentTerms><cbc:Note>#SKONTO#TAGE=14#PROZENT=3.00#</cbc:Note></cac:PaymentTerms><cac:PaymentMeans>",
|
||||
))
|
||||
const detection = await detectElectronicInvoice(xml, { name: "xrechnung.xml" })
|
||||
const parsed = parseElectronicInvoice(detection!.xml, detection!.syntax)
|
||||
assert.deepEqual(parsed.cashDiscount, { percent: 3, days: 14 })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user