// Skonto wird an der Bankzuordnung gespeichert (cashDiscountAmount, gleiches Vorzeichen wie amount). // Zahlbetrag und Skonto gleichen den Beleg gemeinsam aus; der Bankumsatz selbst enthält nur amount. export const CASH_DISCOUNT_RATES = ["19", "7", "0"] as const export const DEFAULT_CASH_DISCOUNT_ACCOUNTS = { received: { "19": "3736", "7": "3731", "0": "3730" }, granted: { "19": "8736", "7": "8731", "0": "8730" }, } export const getAllocationCashDiscount = (allocation: any) => Number(allocation?.cashDiscountAmount || 0) export const getAllocationSettledAmount = (allocation: any) => Number(allocation?.amount || 0) + getAllocationCashDiscount(allocation) export const sumSettledAllocations = (allocations: any[] = []) => (allocations || []).reduce((sum, allocation) => sum + getAllocationSettledAmount(allocation), 0) export const hasCashDiscount = (allocation: any) => !allocation?.archived && Array.isArray(allocation?.cashDiscountSplit) && allocation.cashDiscountSplit.length > 0 // Steuerkorrektur nach § 17 UStG im Zeitraum der Zahlung. // Ausgangsbelege mindern die Umsatzsteuer, Eingangsbelege die Vorsteuer (Vorzeichen aus der Zahlung). export function getCashDiscountTaxBreakdown(allocation: any) { const output = { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 } const input = { ...output } if (!hasCashDiscount(allocation)) return { output, input } const isOutgoing = Boolean(allocation.createddocument || allocation.cd_id) const target = isOutgoing ? output : input const sign = isOutgoing ? -1 : 1 for (const item of allocation.cashDiscountSplit) { const net = Number(item?.net || 0) * sign const tax = Number(item?.tax || 0) * sign if (Number(item?.rate) === 19) { target.net19 += net target.tax19 += tax } else if (Number(item?.rate) === 7) { target.net7 += net target.tax7 += tax } else { target.net0 += net } } return { output, input } } export const resolveCashDiscountAccounts = (config: any) => { const result = { received: { ...DEFAULT_CASH_DISCOUNT_ACCOUNTS.received }, granted: { ...DEFAULT_CASH_DISCOUNT_ACCOUNTS.granted }, } for (const side of ["received", "granted"] as const) { for (const rate of CASH_DISCOUNT_RATES) { const value = config?.[side]?.[rate] if (value !== undefined && value !== null && String(value).trim()) result[side][rate] = String(value).trim() } } return result } // --------------------------------------------------------------------------- // Skonto-Bedingungen auf Belegen (gleiche Logik wie backend/src/modules/cash-discount.ts) // --------------------------------------------------------------------------- const roundMoney = (value: number) => Number(value.toFixed(2)) const formatEuro = (value: number) => new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(value) const formatGermanDate = (value: string) => { const [year, month, day] = value.split("-") return `${day}.${month}.${year}` } const addDays = (date: any, days: number) => { if (!date) return null const iso = typeof date === "string" ? date.slice(0, 10) : new Date(date?.toISOString?.() || date).toISOString().slice(0, 10) const parsed = new Date(`${iso}T00:00:00Z`) if (Number.isNaN(parsed.getTime())) return null parsed.setUTCDate(parsed.getUTCDate() + days) return parsed.toISOString().slice(0, 10) } export function buildCashDiscountTerms({ percent, days, documentDate, dueDate, amount }: any) { const normalizedPercent = Number(percent) if (!Number.isFinite(normalizedPercent) || normalizedPercent <= 0 || !Number.isFinite(Number(amount)) || !Number(amount)) return null const normalizedDays = days === null || days === undefined || days === "" ? null : Number(days) const computedDueDate = dueDate ? String(dueDate).slice(0, 10) : normalizedDays !== null && Number.isFinite(normalizedDays) ? addDays(documentDate, normalizedDays) : null const discount = roundMoney(Number(amount) * normalizedPercent / 100) return { percent: normalizedPercent, days: normalizedDays !== null && Number.isFinite(normalizedDays) ? normalizedDays : null, dueDate: computedDueDate, discount, payable: roundMoney(Number(amount) - discount), } } export function formatCashDiscountText(terms: any) { if (!terms) return "" const percent = String(terms.percent).replace(".", ",") const deadline = terms.dueDate ? `bis zum ${formatGermanDate(terms.dueDate)}` : terms.days !== null ? `innerhalb von ${terms.days} Tagen` : "bei sofortiger Zahlung" return `Bei Zahlung ${deadline} gewähren wir ${percent} % Skonto (${formatEuro(Math.abs(terms.discount))}), Zahlbetrag ${formatEuro(Math.abs(terms.payable))}.` } export function getCashDiscountTemplateContext(terms: any) { if (!terms) return {} return { skonto_prozent: String(terms.percent).replace(".", ","), skonto_tage: terms.days, skonto_datum: terms.dueDate ? formatGermanDate(terms.dueDate) : null, skonto_betrag: formatEuro(Math.abs(terms.discount)), zahlbetrag_mit_skonto: formatEuro(Math.abs(terms.payable)), skonto_hinweis: formatCashDiscountText(terms), } } export function appendCashDiscountText(renderedText: string, template: string | null | undefined, terms: any) { if (!terms || /skonto_/i.test(String(template || ""))) return renderedText const sentence = formatCashDiscountText(terms) return renderedText ? `${sentence}\n\n${renderedText}` : sentence } // Liegt eine Zahlung innerhalb der Skontofrist und passt sie zum Betrag abzüglich Skonto? export function matchCashDiscountPayment({ open, payment, percent, dueDate, paymentDate }: any) { const openAmount = Math.abs(Number(open || 0)) const paid = Math.abs(Number(payment || 0)) const rate = Number(percent || 0) if (!openAmount || !paid || !(rate > 0)) return null if (dueDate && paymentDate && String(paymentDate).slice(0, 10) > String(dueDate).slice(0, 10)) return null const expected = roundMoney(openAmount * (1 - rate / 100)) if (Math.abs(expected - paid) > 0.05) return null return { discount: roundMoney(openAmount - paid), percent: rate } } // Nettowirkung von Skonto für die BWA: gewährter Skonto mindert Erlöse, erhaltener Skonto den Aufwand. // Vorzeichen: Erlösminderung positiv; Aufwand im Vorzeichen der Eingangsbelege (Ausgaben positiv). export function getCashDiscountNetTotals(allocations: any[] = []) { let grantedNet = 0 let receivedNet = 0 let receivedGross = 0 for (const allocation of allocations || []) { if (!hasCashDiscount(allocation)) continue const isOutgoing = Boolean(allocation.createddocument || allocation.cd_id) for (const item of allocation.cashDiscountSplit) { if (isOutgoing) grantedNet += Number(item?.net || 0) else { receivedNet += Number(item?.net || 0) receivedGross += Number(item?.gross || 0) } } } return { grantedNet: roundMoney(grantedNet), // Eingangsbelege: Zahlungen (und damit Skonto) sind negativ, Aufwand wird positiv geführt. receivedNet: roundMoney(-receivedNet), receivedGross: roundMoney(-receivedGross), } }