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:
@@ -34,6 +34,8 @@ const itemInfo = ref({
|
||||
reference: "",
|
||||
date: null,
|
||||
dueDate: null,
|
||||
cashDiscountPercent: null,
|
||||
cashDiscountDueDate: null,
|
||||
paymentType: "Überweisung",
|
||||
description: "",
|
||||
state: "Entwurf",
|
||||
@@ -147,6 +149,28 @@ const setDateFieldToToday = (field) => {
|
||||
itemInfo.value[field] = dayjs().toDate()
|
||||
}
|
||||
|
||||
// Skontofrist als reines Datum speichern, damit der Vergleich mit dem Zahlungsdatum nicht durch Zeitzonen kippt.
|
||||
const setCashDiscountDueDate = (value) => {
|
||||
itemInfo.value.cashDiscountDueDate = value ? dayjs(value.toString()).format("YYYY-MM-DD") : null
|
||||
}
|
||||
|
||||
// Lieferanten-Vorgabe nur bei bewusster Auswahl übernehmen, nicht beim Laden eines Belegs.
|
||||
const applyVendorCashDiscount = (vendorId) => {
|
||||
const vendor = vendors.value.find((item) => item.id === vendorId)
|
||||
if (!vendor?.cashDiscountPercent || itemInfo.value.cashDiscountPercent) return
|
||||
itemInfo.value.cashDiscountPercent = vendor.cashDiscountPercent
|
||||
if (itemInfo.value.date && vendor.cashDiscountDays !== null && vendor.cashDiscountDays !== undefined) {
|
||||
itemInfo.value.cashDiscountDueDate = dayjs(itemInfo.value.date).add(vendor.cashDiscountDays, "day").format("YYYY-MM-DD")
|
||||
}
|
||||
}
|
||||
|
||||
const cashDiscountAmount = computed(() => {
|
||||
const percent = Number(itemInfo.value.cashDiscountPercent || 0)
|
||||
if (!(percent > 0)) return null
|
||||
const gross = (itemInfo.value.accounts || []).reduce((sum, account) => sum + Number(account.amountNet || 0) + Number(account.amountTax || 0), 0)
|
||||
return Number((Math.abs(gross) * percent / 100).toFixed(2))
|
||||
})
|
||||
|
||||
const getDateButtonLabel = (value, emptyLabel = "Kein Datum") => value ? dayjs(value).format('DD.MM.YYYY') : emptyLabel
|
||||
const formatDate = (value) => value ? dayjs(value).format("DD.MM.YYYY") : "-"
|
||||
const formatMoney = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} €`
|
||||
@@ -333,6 +357,9 @@ const updateIncomingInvoice = async (setBooked = false) => {
|
||||
|
||||
let item = { ...itemInfo.value }
|
||||
item.accounts = (item.accounts || []).map((account) => ensureDepreciationDefaults({ ...account }, item.date))
|
||||
const cashDiscountPercent = Number(item.cashDiscountPercent)
|
||||
item.cashDiscountPercent = Number.isFinite(cashDiscountPercent) && cashDiscountPercent > 0 ? cashDiscountPercent : null
|
||||
item.cashDiscountDueDate = item.cashDiscountPercent && item.cashDiscountDueDate ? dayjs(item.cashDiscountDueDate).format("YYYY-MM-DD") : null
|
||||
delete item.files
|
||||
item.state = keepBooked ? "Gebucht" : "Entwurf"
|
||||
|
||||
@@ -569,6 +596,14 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
<dt class="text-gray-500">Fälligkeitsdatum</dt>
|
||||
<dd class="font-medium">{{ formatDate(itemInfo.dueDate) }}</dd>
|
||||
</div>
|
||||
<div v-if="itemInfo.cashDiscountPercent">
|
||||
<dt class="text-gray-500">Skonto</dt>
|
||||
<dd class="font-medium">
|
||||
{{ String(itemInfo.cashDiscountPercent).replace(".", ",") }} %
|
||||
<span v-if="cashDiscountAmount">({{ formatMoney(cashDiscountAmount) }})</span>
|
||||
<span v-if="itemInfo.cashDiscountDueDate"> bis {{ formatDate(itemInfo.cashDiscountDueDate) }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-gray-500">Bankbuchungsdatum</dt>
|
||||
<dd class="font-medium">{{ bankBookingDateLabel }}</dd>
|
||||
@@ -637,6 +672,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
:disabled="mode === 'show'"
|
||||
:filter-fields="['name', 'vendorNumber']"
|
||||
:color="itemInfo.vendor ? 'primary' : 'error'"
|
||||
@update:model-value="applyVendorCashDiscount"
|
||||
>
|
||||
<template #default>
|
||||
{{ vendors.find(v => v.id === itemInfo.vendor)?.name || 'Bitte wählen' }}
|
||||
@@ -657,7 +693,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
v-if="mode !== 'show'"
|
||||
type="vendors"
|
||||
:id="itemInfo.vendor"
|
||||
@return-data="(data) => itemInfo.vendor = data.id"
|
||||
@return-data="(data) => { itemInfo.vendor = data.id; applyVendorCashDiscount(data.id) }"
|
||||
/>
|
||||
</div>
|
||||
</UFormField>
|
||||
@@ -736,6 +772,47 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
</UPopover>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Skonto"
|
||||
:help="cashDiscountAmount ? `Abzug ${formatMoney(cashDiscountAmount)} bei Zahlung in der Frist` : undefined"
|
||||
>
|
||||
<UInput
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="10"
|
||||
placeholder="z. B. 2"
|
||||
v-model="itemInfo.cashDiscountPercent"
|
||||
:disabled="mode === 'show'"
|
||||
class="w-full"
|
||||
>
|
||||
<template #trailing><span class="text-gray-500 text-xs">%</span></template>
|
||||
</UInput>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Skonto bis">
|
||||
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
|
||||
<UButton
|
||||
block
|
||||
icon="i-heroicons-calendar"
|
||||
:label="getDateButtonLabel(itemInfo.cashDiscountDueDate, 'Keine Skontofrist')"
|
||||
:disabled="mode === 'show' || !itemInfo.cashDiscountPercent"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="w-full justify-start"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="p-2">
|
||||
<UCalendar
|
||||
:model-value="getCalendarValue(itemInfo.cashDiscountDueDate)"
|
||||
@update:model-value="setCashDiscountDueDate"
|
||||
:week-starts-on="1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Beschreibung / Notiz" class="md:col-span-2">
|
||||
<UTextarea class="w-full" v-model="itemInfo.description" :rows="2" autoresize :disabled="mode === 'show'" />
|
||||
</UFormField>
|
||||
|
||||
Reference in New Issue
Block a user