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:
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
import { BANK_ALLOCATION_TAX_ITEMS, getBankAllocationAmounts } from "~/utils/bankAllocationTax"
|
||||
import { sumSettledAllocations } from "~/utils/cashDiscount"
|
||||
import { matchCashDiscountPayment, sumSettledAllocations } from "~/utils/cashDiscount"
|
||||
import {
|
||||
DEPRECIATION_METHOD_ITEMS,
|
||||
EXPENSE_BOOKING_MODE_ITEMS,
|
||||
@@ -421,6 +421,30 @@ const isWithinAmountTolerance = (candidateSum, remaining) => {
|
||||
return difference / reference <= 0.1
|
||||
}
|
||||
|
||||
// Erkennt Zahlungen, die genau dem offenen Betrag abzüglich vereinbartem Skonto entsprechen und in der Frist liegen.
|
||||
const getCashDiscountMatch = (kind, entity) => {
|
||||
const paymentDate = itemInfo.value?.date || itemInfo.value?.valueDate
|
||||
if (kind === "createddocument") {
|
||||
const days = entity.cashDiscountDays
|
||||
return matchCashDiscountPayment({
|
||||
open: entity.openSum,
|
||||
payment: calculateOpenSum.value,
|
||||
percent: entity.cashDiscountPercent,
|
||||
dueDate: entity.documentDate && days !== null && days !== undefined && days !== ""
|
||||
? dayjs(entity.documentDate).add(Number(days), "day").format("YYYY-MM-DD")
|
||||
: null,
|
||||
paymentDate: paymentDate ? dayjs(paymentDate).format("YYYY-MM-DD") : null
|
||||
})
|
||||
}
|
||||
return matchCashDiscountPayment({
|
||||
open: getInvoiceSum(entity, true),
|
||||
payment: calculateOpenSum.value,
|
||||
percent: entity.cashDiscountPercent,
|
||||
dueDate: entity.cashDiscountDueDate || null,
|
||||
paymentDate: paymentDate ? dayjs(paymentDate).format("YYYY-MM-DD") : null
|
||||
})
|
||||
}
|
||||
|
||||
const getDocumentSuggestionMeta = (document, suggestion) => {
|
||||
let score = 0
|
||||
const remaining = Math.abs(Number(calculateOpenSum.value))
|
||||
@@ -430,18 +454,22 @@ const getDocumentSuggestionMeta = (document, suggestion) => {
|
||||
document.title,
|
||||
document.description
|
||||
])
|
||||
const amountLabel = getAmountMatchLabel(openSum, remaining)
|
||||
const withinTolerance = isWithinAmountTolerance(openSum, remaining)
|
||||
const cashDiscountMatch = getCashDiscountMatch("createddocument", document)
|
||||
const amountLabel = cashDiscountMatch
|
||||
? `Passt mit ${String(cashDiscountMatch.percent).replace(".", ",")} % Skonto`
|
||||
: getAmountMatchLabel(openSum, remaining)
|
||||
const withinTolerance = cashDiscountMatch || isWithinAmountTolerance(openSum, remaining)
|
||||
|
||||
if (!withinTolerance) {
|
||||
return {
|
||||
score: 0,
|
||||
purposeMatches: [],
|
||||
amountLabel: null
|
||||
amountLabel: null,
|
||||
cashDiscountMatch: null
|
||||
}
|
||||
}
|
||||
|
||||
if (openSum === remaining) score += 100
|
||||
if (openSum === remaining || cashDiscountMatch) score += 100
|
||||
else score += Math.max(0, 60 - Math.abs(openSum - remaining))
|
||||
|
||||
if (suggestion && document.customer?.id === suggestion.id) score += 80
|
||||
@@ -450,7 +478,8 @@ const getDocumentSuggestionMeta = (document, suggestion) => {
|
||||
return {
|
||||
score,
|
||||
purposeMatches,
|
||||
amountLabel
|
||||
amountLabel,
|
||||
cashDiscountMatch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,18 +491,22 @@ const getInvoiceSuggestionMeta = (invoice, suggestion) => {
|
||||
invoice.reference,
|
||||
invoice.description
|
||||
])
|
||||
const amountLabel = getAmountMatchLabel(openSum, remaining)
|
||||
const withinTolerance = isWithinAmountTolerance(openSum, remaining)
|
||||
const cashDiscountMatch = getCashDiscountMatch("incominginvoice", invoice)
|
||||
const amountLabel = cashDiscountMatch
|
||||
? `Passt mit ${String(cashDiscountMatch.percent).replace(".", ",")} % Skonto`
|
||||
: getAmountMatchLabel(openSum, remaining)
|
||||
const withinTolerance = cashDiscountMatch || isWithinAmountTolerance(openSum, remaining)
|
||||
|
||||
if (!withinTolerance) {
|
||||
return {
|
||||
score: 0,
|
||||
purposeMatches: [],
|
||||
amountLabel: null
|
||||
amountLabel: null,
|
||||
cashDiscountMatch: null
|
||||
}
|
||||
}
|
||||
|
||||
if (openSum === remaining) score += 100
|
||||
if (openSum === remaining || cashDiscountMatch) score += 100
|
||||
else score += Math.max(0, 60 - Math.abs(openSum - remaining))
|
||||
|
||||
if (suggestion && invoice.vendor?.id === suggestion.id) score += 80
|
||||
@@ -482,7 +515,8 @@ const getInvoiceSuggestionMeta = (invoice, suggestion) => {
|
||||
return {
|
||||
score,
|
||||
purposeMatches,
|
||||
amountLabel
|
||||
amountLabel,
|
||||
cashDiscountMatch
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +532,7 @@ const suggestedDocuments = computed(() => {
|
||||
suggestionScore: meta.score,
|
||||
suggestionPurposeMatches: meta.purposeMatches,
|
||||
suggestionAmountLabel: meta.amountLabel,
|
||||
suggestionCashDiscount: meta.cashDiscountMatch,
|
||||
suggestionKey: `document:${document.id}`
|
||||
}
|
||||
})
|
||||
@@ -518,6 +553,7 @@ const suggestedIncomingInvoices = computed(() => {
|
||||
suggestionScore: meta.score,
|
||||
suggestionPurposeMatches: meta.purposeMatches,
|
||||
suggestionAmountLabel: meta.amountLabel,
|
||||
suggestionCashDiscount: meta.cashDiscountMatch,
|
||||
suggestionKey: `invoice:${invoice.id}`
|
||||
}
|
||||
})
|
||||
@@ -537,6 +573,7 @@ const fallbackSuggestedDocuments = computed(() => {
|
||||
suggestionScore: meta.score,
|
||||
suggestionPurposeMatches: meta.purposeMatches,
|
||||
suggestionAmountLabel: meta.amountLabel,
|
||||
suggestionCashDiscount: meta.cashDiscountMatch,
|
||||
suggestionKey: `document:${document.id}`
|
||||
}
|
||||
})
|
||||
@@ -557,6 +594,7 @@ const fallbackSuggestedIncomingInvoices = computed(() => {
|
||||
suggestionScore: meta.score,
|
||||
suggestionPurposeMatches: meta.purposeMatches,
|
||||
suggestionAmountLabel: meta.amountLabel,
|
||||
suggestionCashDiscount: meta.cashDiscountMatch,
|
||||
suggestionKey: `invoice:${invoice.id}`
|
||||
}
|
||||
})
|
||||
@@ -1084,7 +1122,7 @@ setup()
|
||||
v-if="canBookCashDiscount('createddocument', document)"
|
||||
size="xs"
|
||||
color="warning"
|
||||
variant="soft"
|
||||
:variant="document.suggestionCashDiscount ? 'solid' : 'soft'"
|
||||
@click="openCashDiscountModal('createddocument', document)"
|
||||
>
|
||||
Mit Skonto
|
||||
@@ -1127,7 +1165,7 @@ setup()
|
||||
v-if="canBookCashDiscount('incominginvoice', invoice)"
|
||||
size="xs"
|
||||
color="warning"
|
||||
variant="soft"
|
||||
:variant="invoice.suggestionCashDiscount ? 'solid' : 'soft'"
|
||||
@click="openCashDiscountModal('incominginvoice', invoice)"
|
||||
>
|
||||
Mit Skonto
|
||||
@@ -1182,6 +1220,7 @@ setup()
|
||||
<div class="text-right">
|
||||
<div class="font-mono font-bold text-sm">{{ displayCurrency(document.openSum) }}</div>
|
||||
<div class="text-xs text-gray-400">Offen</div>
|
||||
<UBadge v-if="getCashDiscountMatch('createddocument', document)" size="xs" color="warning" variant="subtle">Skonto passt</UBadge>
|
||||
</div>
|
||||
<UTooltip text="Zuweisen">
|
||||
<UButton
|
||||
@@ -1251,6 +1290,7 @@ setup()
|
||||
{{ displayCurrency(getInvoiceSum(item, true)) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-400">Offen</div>
|
||||
<UBadge v-if="getCashDiscountMatch('incominginvoice', item)" size="xs" color="warning" variant="subtle">Skonto passt</UBadge>
|
||||
</div>
|
||||
<UTooltip text="Zuweisen">
|
||||
<UButton
|
||||
|
||||
Reference in New Issue
Block a user