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,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs"
|
||||
import { getBankAllocationTaxBreakdown } from "~/utils/bankAllocationTax"
|
||||
import { getCashDiscountNetTotals } from "~/utils/cashDiscount"
|
||||
import {
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown
|
||||
@@ -90,7 +91,7 @@ const loadSummary = async () => {
|
||||
return touchesAccount && date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
})
|
||||
|
||||
const income = outputDocs.reduce((sum: number, doc: any) => {
|
||||
const documentIncome = outputDocs.reduce((sum: number, doc: any) => {
|
||||
return sum + (doc.rows || []).reduce((rowSum: number, row: any) => {
|
||||
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) {
|
||||
return rowSum
|
||||
@@ -132,7 +133,14 @@ const loadSummary = async () => {
|
||||
const { output, input } = getBankAllocationTaxBreakdown(allocation)
|
||||
return sum + output.tax19 + output.tax7 - input.tax19 - input.tax7
|
||||
}, 0)
|
||||
const expenses = invoiceExpenses + directAccountExpenses + depreciations
|
||||
// Skonto wirkt im Monat der Zahlung auf Erlöse und Aufwand.
|
||||
const cashDiscountTotals = getCashDiscountNetTotals((allocations || []).filter((allocation: any) => {
|
||||
const date = dayjs(allocation.bankstatement?.date || allocation.bankstatement?.valueDate)
|
||||
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
||||
}))
|
||||
const expenses = invoiceExpenses + directAccountExpenses + depreciations - cashDiscountTotals.receivedNet
|
||||
|
||||
const income = documentIncome - cashDiscountTotals.grantedNet
|
||||
|
||||
summary.value = {
|
||||
label: dayjs().format("MMMM YYYY"),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { getCashDiscountNetTotals } from "~/utils/cashDiscount"
|
||||
import dayjs from "dayjs"
|
||||
import { getBankAllocationAmounts, getBankAllocationTaxBreakdown } from "~/utils/bankAllocationTax"
|
||||
import {
|
||||
@@ -183,8 +184,12 @@ const selectedPeriodBounds = computed(() => {
|
||||
return { start, end }
|
||||
})
|
||||
|
||||
// Skonto wirkt im Zeitraum der Zahlung: gewährter Skonto mindert Erlöse, erhaltener Skonto den Aufwand.
|
||||
const cashDiscountTotals = computed(() => getCashDiscountNetTotals(filteredStatementAllocations.value))
|
||||
|
||||
const incomeTotal = computed(() => {
|
||||
return Number(filteredDocuments.value.reduce((sum, doc) => sum + computeDocumentNet(doc), 0).toFixed(2))
|
||||
const documents = filteredDocuments.value.reduce((sum, doc) => sum + computeDocumentNet(doc), 0)
|
||||
return Number((documents - cashDiscountTotals.value.grantedNet).toFixed(2))
|
||||
})
|
||||
|
||||
const expenseNetTotal = computed(() => {
|
||||
@@ -196,7 +201,7 @@ const expenseNetTotal = computed(() => {
|
||||
|
||||
const depreciations = depreciationTotal.value
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses + depreciations).toFixed(2))
|
||||
return Number((invoiceExpenses + directAccountExpenses + depreciations - cashDiscountTotals.value.receivedNet).toFixed(2))
|
||||
})
|
||||
|
||||
const expenseGrossTotal = computed(() => {
|
||||
@@ -206,7 +211,7 @@ const expenseGrossTotal = computed(() => {
|
||||
|
||||
const depreciations = depreciationTotal.value
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses + depreciations).toFixed(2))
|
||||
return Number((invoiceExpenses + directAccountExpenses + depreciations - cashDiscountTotals.value.receivedGross).toFixed(2))
|
||||
})
|
||||
|
||||
const taxSummary = computed(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { sumSettledAllocations } from "~/utils/cashDiscount"
|
||||
import { matchCashDiscountPayment, sumSettledAllocations } from "~/utils/cashDiscount"
|
||||
import { parseDate } from "@internationalized/date"
|
||||
|
||||
const {$api, $dayjs} = useNuxtApp()
|
||||
@@ -387,25 +387,52 @@ const getTopEntitySuggestion = (statement) => {
|
||||
return suggestions[0] || null
|
||||
}
|
||||
|
||||
// Zahlung entspricht dem offenen Betrag abzüglich vereinbartem Skonto und liegt in der Frist.
|
||||
const getCashDiscountMatch = (statement, kind, entity) => {
|
||||
const paymentDate = getStatementDate(statement) ? $dayjs(getStatementDate(statement)).format("YYYY-MM-DD") : null
|
||||
if (kind === "createddocument") {
|
||||
const days = entity.cashDiscountDays
|
||||
return matchCashDiscountPayment({
|
||||
open: entity.openSum,
|
||||
payment: calculateOpenSum(statement),
|
||||
percent: entity.cashDiscountPercent,
|
||||
dueDate: entity.documentDate && days !== null && days !== undefined && days !== ""
|
||||
? $dayjs(entity.documentDate).add(Number(days), "day").format("YYYY-MM-DD")
|
||||
: null,
|
||||
paymentDate
|
||||
})
|
||||
}
|
||||
return matchCashDiscountPayment({
|
||||
open: getInvoiceSum(entity, true),
|
||||
payment: calculateOpenSum(statement),
|
||||
percent: entity.cashDiscountPercent,
|
||||
dueDate: entity.cashDiscountDueDate || null,
|
||||
paymentDate
|
||||
})
|
||||
}
|
||||
|
||||
const getDocumentSuggestionMeta = (statement, document, suggestion) => {
|
||||
let score = 0
|
||||
const remaining = Math.abs(Number(calculateOpenSum(statement)))
|
||||
const openSum = Math.abs(Number(document.openSum))
|
||||
const purposeMatches = getMatchingPurposeParts(statement.text, [document.documentNumber, document.title, document.description])
|
||||
const amountLabel = getAmountMatchLabel(openSum, remaining)
|
||||
const withinTolerance = isWithinAmountTolerance(openSum, remaining)
|
||||
const cashDiscountMatch = getCashDiscountMatch(statement, "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, amountLabel: null, purposeMatches: [] }
|
||||
return { score: 0, amountLabel: null, purposeMatches: [], 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
|
||||
if (purposeMatches.length > 0) score += 50 + (purposeMatches.length * 10)
|
||||
|
||||
return { score, amountLabel, purposeMatches }
|
||||
return { score, amountLabel, purposeMatches, cashDiscountMatch }
|
||||
}
|
||||
|
||||
const getIncomingInvoiceSuggestionMeta = (statement, invoice, suggestion) => {
|
||||
@@ -413,20 +440,23 @@ const getIncomingInvoiceSuggestionMeta = (statement, invoice, suggestion) => {
|
||||
const remaining = Math.abs(Number(calculateOpenSum(statement)))
|
||||
const openSum = Math.abs(Number(getInvoiceSum(invoice, true)))
|
||||
const purposeMatches = getMatchingPurposeParts(statement.text, [invoice.reference, invoice.description])
|
||||
const amountLabel = getAmountMatchLabel(openSum, remaining)
|
||||
const withinTolerance = isWithinAmountTolerance(openSum, remaining)
|
||||
const cashDiscountMatch = getCashDiscountMatch(statement, "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, amountLabel: null, purposeMatches: [] }
|
||||
return { score: 0, amountLabel: null, purposeMatches: [], 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
|
||||
if (purposeMatches.length > 0) score += 50 + (purposeMatches.length * 10)
|
||||
|
||||
return { score, amountLabel, purposeMatches }
|
||||
return { score, amountLabel, purposeMatches, cashDiscountMatch }
|
||||
}
|
||||
|
||||
const getRowSuggestions = (statement) => {
|
||||
@@ -435,7 +465,7 @@ const getRowSuggestions = (statement) => {
|
||||
const topDocument = (entitySuggestion?.type === "customer" ? openDocuments.value.filter(doc => doc.customer?.id === entitySuggestion.id) : openDocuments.value)
|
||||
.map(document => {
|
||||
const meta = getDocumentSuggestionMeta(statement, document, entitySuggestion?.type === "customer" ? entitySuggestion : null)
|
||||
return { ...document, suggestionScore: meta.score, suggestionAmountLabel: meta.amountLabel, suggestionPurposeMatches: meta.purposeMatches, suggestionKey: `document:${document.id}` }
|
||||
return { ...document, suggestionScore: meta.score, suggestionAmountLabel: meta.amountLabel, suggestionPurposeMatches: meta.purposeMatches, suggestionCashDiscount: meta.cashDiscountMatch, suggestionKey: `document:${document.id}` }
|
||||
})
|
||||
.filter(document => !isSuggestionDismissed(statement.id, document.suggestionKey))
|
||||
.filter(document => document.suggestionScore >= 80 || document.suggestionPurposeMatches?.length > 0)
|
||||
@@ -444,7 +474,7 @@ const getRowSuggestions = (statement) => {
|
||||
const topIncomingInvoice = (entitySuggestion?.type === "vendor" ? openIncomingInvoices.value.filter(invoice => invoice.vendor?.id === entitySuggestion.id) : openIncomingInvoices.value)
|
||||
.map(invoice => {
|
||||
const meta = getIncomingInvoiceSuggestionMeta(statement, invoice, entitySuggestion?.type === "vendor" ? entitySuggestion : null)
|
||||
return { ...invoice, suggestionScore: meta.score, suggestionAmountLabel: meta.amountLabel, suggestionPurposeMatches: meta.purposeMatches, suggestionKey: `invoice:${invoice.id}` }
|
||||
return { ...invoice, suggestionScore: meta.score, suggestionAmountLabel: meta.amountLabel, suggestionPurposeMatches: meta.purposeMatches, suggestionCashDiscount: meta.cashDiscountMatch, suggestionKey: `invoice:${invoice.id}` }
|
||||
})
|
||||
.filter(invoice => !isSuggestionDismissed(statement.id, invoice.suggestionKey))
|
||||
.filter(invoice => invoice.suggestionScore >= 80 || invoice.suggestionPurposeMatches?.length > 0)
|
||||
@@ -552,6 +582,24 @@ const handleAssignIncomingInvoice = async (row, invoice, event) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Bucht die Zahlung zusammen mit dem vereinbarten Skonto, wenn Betrag und Frist zu den Skontobedingungen passen.
|
||||
const handleAssignWithCashDiscount = async (row, kind, entity, event) => {
|
||||
event.stopPropagation()
|
||||
const match = entity.suggestionCashDiscount
|
||||
if (!match) return
|
||||
try {
|
||||
await saveAllocation({
|
||||
[kind]: entity.id,
|
||||
bankstatement: row.id,
|
||||
amount: Number(calculateOpenSum(row)),
|
||||
cashDiscountAmount: match.discount,
|
||||
description: "Automatischer Vorschlag mit Skonto"
|
||||
})
|
||||
} catch (err) {
|
||||
toast.add({ title: "Skonto konnte nicht gebucht werden", description: err?.data?.error, color: "error" })
|
||||
}
|
||||
}
|
||||
|
||||
const dismissSuggestion = (row, suggestionKey, event) => {
|
||||
event.stopPropagation()
|
||||
const bankingSettings = tempStore.settings?.banking || {}
|
||||
@@ -928,7 +976,15 @@ onMounted(() => {
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
<UButton size="sm" color="emerald" @click="handleAssignDocument(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topDocument, $event)">
|
||||
<UButton
|
||||
v-if="selectedSuggestionRow.suggestions.topDocument.suggestionCashDiscount"
|
||||
size="sm"
|
||||
color="warning"
|
||||
@click="handleAssignWithCashDiscount(selectedSuggestionRow.row, 'createddocument', selectedSuggestionRow.suggestions.topDocument, $event)"
|
||||
>
|
||||
Mit Skonto zuweisen
|
||||
</UButton>
|
||||
<UButton v-else size="sm" color="emerald" @click="handleAssignDocument(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topDocument, $event)">
|
||||
Zuweisen
|
||||
</UButton>
|
||||
<UButton size="sm" color="gray" variant="ghost" @click="dismissSuggestion(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topDocument.suggestionKey, $event)">
|
||||
@@ -951,7 +1007,15 @@ onMounted(() => {
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
<UButton size="sm" color="error" @click="handleAssignIncomingInvoice(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice, $event)">
|
||||
<UButton
|
||||
v-if="selectedSuggestionRow.suggestions.topIncomingInvoice.suggestionCashDiscount"
|
||||
size="sm"
|
||||
color="warning"
|
||||
@click="handleAssignWithCashDiscount(selectedSuggestionRow.row, 'incominginvoice', selectedSuggestionRow.suggestions.topIncomingInvoice, $event)"
|
||||
>
|
||||
Mit Skonto zuweisen
|
||||
</UButton>
|
||||
<UButton v-else size="sm" color="error" @click="handleAssignIncomingInvoice(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice, $event)">
|
||||
Zuweisen
|
||||
</UButton>
|
||||
<UButton size="sm" color="gray" variant="ghost" @click="dismissSuggestion(selectedSuggestionRow.row, selectedSuggestionRow.suggestions.topIncomingInvoice.suggestionKey, $event)">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@ import {useFunctions} from "~/composables/useFunctions.js";
|
||||
import EntityModalButtons from "~/components/EntityModalButtons.vue";
|
||||
import { documentTemplateHandlebars } from "~/utils/handlebars";
|
||||
import { getTitleSectionMove, moveTitleSection } from "~/utils/documentTitleSections.js";
|
||||
import { appendCashDiscountText, buildCashDiscountTerms, formatCashDiscountText, getCashDiscountTemplateContext } from "~/utils/cashDiscount";
|
||||
|
||||
const dataStore = useDataStore()
|
||||
const profileStore = useProfileStore()
|
||||
@@ -57,6 +58,8 @@ const itemInfo = ref({
|
||||
deliveryDateType: "Lieferdatum",
|
||||
dateOfPerformance: null,
|
||||
paymentDays: auth.activeTenantData.standardPaymentDays,
|
||||
cashDiscountPercent: null,
|
||||
cashDiscountDays: null,
|
||||
payment_type: "transfer",
|
||||
outgoingsepamandate: null,
|
||||
availableInPortal: false,
|
||||
@@ -635,7 +638,11 @@ const setupPage = async () => {
|
||||
if (optionsToImport.deliveryDate) itemInfo.value.deliveryDate = linkedDocument.deliveryDate
|
||||
if (optionsToImport.deliveryDateEnd) itemInfo.value.deliveryDateEnd = linkedDocument.deliveryDateEnd
|
||||
if (optionsToImport.documentDate) itemInfo.value.documentDate = linkedDocument.documentDate
|
||||
if (optionsToImport.paymentDays) itemInfo.value.paymentDays = linkedDocument.paymentDays
|
||||
if (optionsToImport.paymentDays) {
|
||||
itemInfo.value.paymentDays = linkedDocument.paymentDays
|
||||
itemInfo.value.cashDiscountPercent = linkedDocument.cashDiscountPercent ?? null
|
||||
itemInfo.value.cashDiscountDays = linkedDocument.cashDiscountDays ?? null
|
||||
}
|
||||
if (optionsToImport.payment_type) itemInfo.value.payment_type = linkedDocument.payment_type
|
||||
if (optionsToImport.customSurchargePercentage) itemInfo.value.customSurchargePercentage = linkedDocument.customSurchargePercentage
|
||||
if (optionsToImport.contactPerson) itemInfo.value.contactPerson = linkedDocument.contactPerson
|
||||
@@ -663,6 +670,8 @@ const setupPage = async () => {
|
||||
itemInfo.value.deliveryDateEnd = linkedDocument.deliveryDateEnd
|
||||
itemInfo.value.documentDate = linkedDocument.documentDate
|
||||
itemInfo.value.paymentDays = linkedDocument.paymentDays
|
||||
itemInfo.value.cashDiscountPercent = linkedDocument.cashDiscountPercent ?? null
|
||||
itemInfo.value.cashDiscountDays = linkedDocument.cashDiscountDays ?? null
|
||||
itemInfo.value.payment_type = linkedDocument.payment_type
|
||||
itemInfo.value.customSurchargePercentage = linkedDocument.customSurchargePercentage
|
||||
itemInfo.value.contactPerson = linkedDocument.contactPerson
|
||||
@@ -833,6 +842,10 @@ const setCustomerData = async (customerId, loadOnlyAdress = false) => {
|
||||
itemInfo.value.address.country = customer.infoData.country
|
||||
|
||||
if (!loadOnlyAdress && customer.customPaymentDays) itemInfo.value.paymentDays = customer.customPaymentDays
|
||||
if (!loadOnlyAdress && customer.customCashDiscountPercent) {
|
||||
itemInfo.value.cashDiscountPercent = customer.customCashDiscountPercent
|
||||
itemInfo.value.cashDiscountDays = customer.customCashDiscountDays ?? null
|
||||
}
|
||||
if (!loadOnlyAdress && customer.custom_payment_type) itemInfo.value.payment_type = customer.custom_payment_type
|
||||
if (!loadOnlyAdress) {
|
||||
itemInfo.value.taxType = normalizeTaxTypeValue(customer.customTaxType)
|
||||
@@ -1357,6 +1370,17 @@ const documentTotal = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Skonto bezieht sich bei Schlussrechnungen auf die Restforderung nach Abzug der Abschläge.
|
||||
const cashDiscountTerms = computed(() => {
|
||||
if (!["invoices", "advanceInvoices", "serialInvoices"].includes(itemInfo.value.type)) return null
|
||||
return buildCashDiscountTerms({
|
||||
percent: itemInfo.value.cashDiscountPercent,
|
||||
days: itemInfo.value.cashDiscountDays,
|
||||
documentDate: itemInfo.value.documentDate ? dayjs(itemInfo.value.documentDate).format("YYYY-MM-DD") : null,
|
||||
amount: itemInfo.value.type === "invoices" ? documentTotal.value.totalSumToPay : documentTotal.value.totalGross
|
||||
})
|
||||
})
|
||||
|
||||
const documentReport = computed(() => {
|
||||
|
||||
let totalProductsPurchasePrice = 0
|
||||
@@ -1574,7 +1598,8 @@ const getDocumentData = async () => {
|
||||
kundenname: customerData && customerData.name,
|
||||
zahlungsziel_in_tagen: itemInfo.paymentDays,
|
||||
zahlungsart: itemInfo.payment_type === "transfer" ? "Überweisung" : "Lastschrift",
|
||||
diesel_gesamtverbrauch: (itemInfo.agriculture && itemInfo.agriculture.dieselUsageTotal) && itemInfo.agriculture.dieselUsageTotal
|
||||
diesel_gesamtverbrauch: (itemInfo.agriculture && itemInfo.agriculture.dieselUsageTotal) && itemInfo.agriculture.dieselUsageTotal,
|
||||
...getCashDiscountTemplateContext(cashDiscountTerms.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1684,7 +1709,7 @@ const getDocumentData = async () => {
|
||||
],
|
||||
title: itemInfo.value.title,
|
||||
description: itemInfo.value.description,
|
||||
endText: templateEndText(generateContext(itemInfo.value, contactData)),
|
||||
endText: appendCashDiscountText(templateEndText(generateContext(itemInfo.value, contactData)), itemInfo.value.endText, cashDiscountTerms.value),
|
||||
startText: templateStartText(generateContext(itemInfo.value, contactData)),
|
||||
rows: rows,
|
||||
totalArray: [
|
||||
@@ -1819,6 +1844,8 @@ const saveSerialInvoice = async () => {
|
||||
project: normalizeEntityId(itemInfo.value.project),
|
||||
costcentre: itemInfo.value.costcentre,
|
||||
paymentDays: itemInfo.value.paymentDays,
|
||||
cashDiscountPercent: itemInfo.value.cashDiscountPercent === "" || itemInfo.value.cashDiscountPercent === null ? null : Number(itemInfo.value.cashDiscountPercent),
|
||||
cashDiscountDays: itemInfo.value.cashDiscountDays === "" || itemInfo.value.cashDiscountDays === null ? null : Number(itemInfo.value.cashDiscountDays),
|
||||
payment_type: itemInfo.value.payment_type,
|
||||
outgoingsepamandate: normalizeEntityId(itemInfo.value.outgoingsepamandate),
|
||||
deliveryDateType: "Leistungszeitraum",
|
||||
@@ -1935,6 +1962,8 @@ const saveDocument = async (state, resetup = false) => {
|
||||
deliveryDate: itemInfo.value.deliveryDate,
|
||||
deliveryDateEnd: itemInfo.value.deliveryDateEnd,
|
||||
paymentDays: itemInfo.value.paymentDays,
|
||||
cashDiscountPercent: itemInfo.value.cashDiscountPercent === "" || itemInfo.value.cashDiscountPercent === null ? null : Number(itemInfo.value.cashDiscountPercent),
|
||||
cashDiscountDays: itemInfo.value.cashDiscountDays === "" || itemInfo.value.cashDiscountDays === null ? null : Number(itemInfo.value.cashDiscountDays),
|
||||
payment_type: itemInfo.value.payment_type,
|
||||
outgoingsepamandate: normalizeEntityId(itemInfo.value.outgoingsepamandate),
|
||||
deliveryDateType: itemInfo.value.deliveryDateType,
|
||||
@@ -2690,6 +2719,39 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
v-if="['invoices', 'advanceInvoices', 'serialInvoices'].includes(itemInfo.type)"
|
||||
class="min-w-0 flex-1"
|
||||
label="Skonto:"
|
||||
:help="cashDiscountTerms ? formatCashDiscountText(cashDiscountTerms) : undefined"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<UInput
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="10"
|
||||
placeholder="%"
|
||||
v-model="itemInfo.cashDiscountPercent"
|
||||
class="w-full"
|
||||
>
|
||||
<template #trailing>
|
||||
<span class="text-gray-500 dark:text-gray-400 text-xs">%</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UInput
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="Tage"
|
||||
v-model="itemInfo.cashDiscountDays"
|
||||
class="w-full"
|
||||
>
|
||||
<template #trailing>
|
||||
<span class="text-gray-500 dark:text-gray-400 text-xs">Tage</span>
|
||||
</template>
|
||||
</UInput>
|
||||
</div>
|
||||
</UFormField>
|
||||
<UFormField
|
||||
class="min-w-0 flex-1"
|
||||
label="Individueller Aufschlag:"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -29,12 +29,18 @@ const variableDefinitions = [
|
||||
{ key: '{{titel}}', label: 'Titel', desc: 'Titel des Kunden' },
|
||||
{ key: '{{zahlungsziel_in_tagen}}', label: 'Zahlungsziel', desc: 'In Tagen' },
|
||||
{ key: '{{lohnkosten}}', label: 'Lohnkosten', desc: 'Ausgewiesene Lohnkosten' },
|
||||
{ key: '{{skonto_hinweis}}', label: 'Skonto-Hinweis', desc: 'Kompletter Satz; ohne Skontovariable wird er automatisch vor den Endtext gesetzt' },
|
||||
{ key: '{{skonto_prozent}}', label: 'Skonto %', desc: 'Skontosatz' },
|
||||
{ key: '{{skonto_datum}}', label: 'Skonto bis', desc: 'Datum der Skontofrist' },
|
||||
{ key: '{{skonto_betrag}}', label: 'Skontobetrag', desc: 'Abzugsbetrag in Euro' },
|
||||
{ key: '{{zahlbetrag_mit_skonto}}', label: 'Zahlbetrag mit Skonto', desc: 'Rechnungsbetrag abzüglich Skonto' },
|
||||
]
|
||||
|
||||
const conditionalExamples = [
|
||||
'{{#if vorname}}Hallo {{vorname}},{{/if}}',
|
||||
'{{#if (eq zahlungsart "Überweisung")}}Bitte überweisen Sie den Betrag.{{/if}}',
|
||||
'{{#if (gt zahlungsziel_in_tagen 14)}}Vielen Dank für Ihre Zahlung innerhalb des erweiterten Zahlungsziels.{{/if}}',
|
||||
'{{#if skonto_prozent}}Bei Zahlung bis {{skonto_datum}} ziehen Sie bitte {{skonto_prozent}} % Skonto ab.{{/if}}',
|
||||
]
|
||||
|
||||
// --- Shortcuts ---
|
||||
|
||||
@@ -345,6 +345,18 @@ export const useDataStore = defineStore('data', () => {
|
||||
inputType: "number",
|
||||
inputColumn: "Allgemeines",
|
||||
sortable: true
|
||||
},{
|
||||
key: "customCashDiscountPercent",
|
||||
label: "Skonto in %",
|
||||
inputType: "number",
|
||||
inputColumn: "Allgemeines",
|
||||
disabledInTable: true
|
||||
},{
|
||||
key: "customCashDiscountDays",
|
||||
label: "Skontofrist in Tagen",
|
||||
inputType: "number",
|
||||
inputColumn: "Allgemeines",
|
||||
disabledInTable: true
|
||||
},{
|
||||
key: "custom_payment_type",
|
||||
label: "Zahlungsart",
|
||||
@@ -2007,6 +2019,18 @@ export const useDataStore = defineStore('data', () => {
|
||||
inputType: "bool",
|
||||
component: sepa
|
||||
},
|
||||
{
|
||||
key: "cashDiscountPercent",
|
||||
label: "Skonto in %",
|
||||
inputType: "number",
|
||||
disabledInTable: true
|
||||
},
|
||||
{
|
||||
key: "cashDiscountDays",
|
||||
label: "Skontofrist in Tagen",
|
||||
inputType: "number",
|
||||
disabledInTable: true
|
||||
},
|
||||
{
|
||||
key: "defaultPaymentMethod",
|
||||
label: "Standard Zahlungsmethode",
|
||||
|
||||
@@ -59,3 +59,104 @@ export const resolveCashDiscountAccounts = (config: any) => {
|
||||
}
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user