Skonto wird an der Bankzuordnung gespeichert und gleicht den Beleg zusammen mit der Zahlung aus. Der Betrag wird anteilig nach Steuersätzen auf die Skontokonten verteilt (Standard SKR03, je Mandant einstellbar), im DATEV-Export als eigene Buchung je Steuersatz ausgegeben und in der USt-Auswertung im Zeitraum der Zahlung berücksichtigt. Schlussrechnungen verteilen den Skonto auf die Restforderung nach Abzug der Abschläge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
5.6 KiB
TypeScript
117 lines
5.6 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import test from "node:test"
|
|
import {
|
|
getCreatedDocumentGrossByRate,
|
|
getCreatedDocumentGrossTotal,
|
|
getIncomingInvoiceGrossByRate,
|
|
normalizeCashDiscountAccountsInput,
|
|
resolveCashDiscountAccounts,
|
|
splitCashDiscount,
|
|
validateCashDiscount,
|
|
} from "../src/modules/cash-discount"
|
|
import { getCashDiscountBookingLines } from "../src/utils/export/datev"
|
|
import { prepareStatementAllocationInput } from "../src/mcp/statementAllocations"
|
|
|
|
const defaults = resolveCashDiscountAccounts({})
|
|
|
|
test("splits incoming invoice discount by tax rate onto received discount accounts", () => {
|
|
const gross = getIncomingInvoiceGrossByRate({
|
|
expense: true,
|
|
accounts: [
|
|
{ taxType: "19", amountNet: 100, amountTax: 19 },
|
|
{ taxType: "7", amountNet: 100, amountTax: 7 },
|
|
],
|
|
})
|
|
assert.deepEqual(gross, { "19": -119, "7": -107, "0": 0 })
|
|
|
|
const split = splitCashDiscount(-4.52, gross, defaults.received)
|
|
assert.deepEqual(split, [
|
|
{ rate: 19, account: "3736", gross: -2.38, net: -2, tax: -0.38 },
|
|
{ rate: 7, account: "3731", gross: -2.14, net: -2, tax: -0.14 },
|
|
])
|
|
})
|
|
|
|
test("assigns rounding differences to the largest share", () => {
|
|
const split = splitCashDiscount(1, { "19": 1, "7": 1, "0": 1 }, defaults.granted)
|
|
assert.equal(split.reduce((sum, item) => sum + item.gross, 0).toFixed(2), "1.00")
|
|
assert.equal(split.length, 3)
|
|
})
|
|
|
|
test("rejects discounts on intra-community acquisitions", () => {
|
|
assert.throws(
|
|
() => getIncomingInvoiceGrossByRate({ accounts: [{ taxType: "19I", amountNet: 100, amountTax: 0 }] }),
|
|
/manuell buchen/,
|
|
)
|
|
})
|
|
|
|
test("final invoices spread discounts over the remaining claim after advance invoices", () => {
|
|
const advance = { id: 7, rows: [{ advanceInvoiceData: true, price: 1000, taxPercent: 19 }] }
|
|
const final = {
|
|
id: 8,
|
|
usedAdvanceInvoices: [7],
|
|
rows: [
|
|
{ mode: "normal", quantity: 1, price: 2000, discountPercent: 0, taxPercent: 19 },
|
|
{ mode: "normal", quantity: 1, price: 1000, discountPercent: 0, taxPercent: 7 },
|
|
],
|
|
}
|
|
assert.deepEqual(getCreatedDocumentGrossByRate(final, [advance]), { "19": 1190, "7": 1070, "0": 0 })
|
|
assert.equal(getCreatedDocumentGrossTotal(final, [advance]), 2260)
|
|
})
|
|
|
|
test("tax exempt small business invoices use the account without tax", () => {
|
|
const gross = getCreatedDocumentGrossByRate({
|
|
taxType: "19 UStG",
|
|
rows: [{ mode: "normal", quantity: 1, price: 100, discountPercent: 0, taxPercent: 19 }],
|
|
})
|
|
assert.deepEqual(gross, { "19": 0, "7": 0, "0": 100 })
|
|
assert.deepEqual(splitCashDiscount(2, gross, defaults.granted), [{ rate: 0, account: "8730", gross: 2, net: 2, tax: 0 }])
|
|
})
|
|
|
|
test("validates payment plus discount against the open amount", () => {
|
|
assert.doesNotThrow(() => validateCashDiscount({ amount: 98, discount: 2, total: 100, settled: 0 }))
|
|
assert.doesNotThrow(() => validateCashDiscount({ amount: -98, discount: -2, total: -100, settled: 0 }))
|
|
assert.throws(() => validateCashDiscount({ amount: 99, discount: 2, total: 100, settled: 0 }), /offenen Betrag/)
|
|
assert.throws(() => validateCashDiscount({ amount: 50, discount: 2, total: 100, settled: 50 }), /offenen Betrag/)
|
|
assert.throws(() => validateCashDiscount({ amount: 80, discount: 20, total: 100, settled: 0 }), /höher als 10 %/)
|
|
assert.throws(() => validateCashDiscount({ amount: -98, discount: -2, total: 100, settled: 0 }), /Richtung/)
|
|
})
|
|
|
|
test("normalizes configured discount accounts and keeps SKR03 defaults", () => {
|
|
const input = normalizeCashDiscountAccountsInput({ received: { "19": " 3737 ", "7": "" } })
|
|
assert.deepEqual(input, { received: { "19": "3737" } })
|
|
assert.equal(resolveCashDiscountAccounts(input).received["19"], "3737")
|
|
assert.equal(resolveCashDiscountAccounts(input).received["7"], "3731")
|
|
assert.throws(() => normalizeCashDiscountAccountsInput({ granted: { "19": "87A6" } }), /Ziffern/)
|
|
})
|
|
|
|
test("exports discount bookings per tax rate against the business partner", () => {
|
|
const lines = getCashDiscountBookingLines({
|
|
bankstatement: { id: 42, date: "2026-10-05" },
|
|
incominginvoice: { reference: "RE-1", vendor: { vendorNumber: "70001", name: "Lieferant" } },
|
|
cashDiscountSplit: [
|
|
{ rate: 19, account: "3736", gross: -2.38, net: -2, tax: -0.38 },
|
|
{ rate: 7, account: "3731", gross: -2.14, net: -2, tax: -0.14 },
|
|
],
|
|
}).map(line => line.split(";"))
|
|
|
|
assert.equal(lines.length, 2)
|
|
assert.equal(lines[0].length, 124)
|
|
assert.deepEqual([lines[0][0], lines[0][1], lines[0][6], lines[0][7], lines[0][9], lines[0][10]],
|
|
["2,38", '"H"', "3736", "70001", "0510", '"RE-1"'])
|
|
|
|
const [granted] = getCashDiscountBookingLines({
|
|
bankstatement: { id: 43, date: "2026-10-05" },
|
|
createddocument: { documentNumber: "RE-100", customer: { customerNumber: "10001", name: "Kunde" } },
|
|
cashDiscountSplit: [{ rate: 19, account: "8736", gross: 2.38, net: 2, tax: 0.38 }],
|
|
}).map(line => line.split(";"))
|
|
assert.deepEqual([granted[0], granted[1], granted[6], granted[7]], ["2,38", '"S"', "8736", "10001"])
|
|
})
|
|
|
|
test("MCP allocation input accepts cash discounts only with bank statements", () => {
|
|
const payload = prepareStatementAllocationInput({ bankstatement: 1, createddocument: 2, amount: 98, cashDiscountAmount: 2 })
|
|
assert.equal(payload.cashDiscountAmount, 2)
|
|
assert.throws(() => prepareStatementAllocationInput({
|
|
amount: 98, cashDiscountAmount: 2, manualBookingDate: "2026-10-01", account: 1, contraAccount: 2,
|
|
}), /Bankzahlung/)
|
|
})
|