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>
329 lines
12 KiB
Vue
329 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import dayjs from "dayjs"
|
|
import { getBankAllocationTaxBreakdown, isDirectBankAllocation } from "~/utils/bankAllocationTax"
|
|
import { hasCashDiscount } from "~/utils/cashDiscount"
|
|
import customParseFormat from "dayjs/plugin/customParseFormat"
|
|
import {
|
|
formatTaxEvaluationPeriodLabel,
|
|
formatTaxEvaluationPeriodRange,
|
|
getCreatedDocumentTaxBreakdown,
|
|
getIncomingInvoiceTaxBreakdown,
|
|
getManualBookingTaxBreakdown,
|
|
getTaxEvaluationPeriodBounds,
|
|
normalizeTaxEvaluationPeriod,
|
|
shiftTaxEvaluationPeriodStart
|
|
} from "~/composables/useTaxEvaluation"
|
|
|
|
dayjs.extend(customParseFormat)
|
|
|
|
const auth = useAuthStore()
|
|
|
|
const loading = ref(true)
|
|
const createdDocuments = ref<any[]>([])
|
|
const incomingInvoices = ref<any[]>([])
|
|
const manualBookings = ref<any[]>([])
|
|
const bankAllocations = ref<any[]>([])
|
|
|
|
const periodType = computed(() => normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod))
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat("de-DE", {
|
|
style: "currency",
|
|
currency: "EUR"
|
|
}).format(Number(value || 0))
|
|
}
|
|
|
|
const isRelevantOutputDocument = (doc: any) => {
|
|
return doc?.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)
|
|
}
|
|
|
|
const isRelevantInputInvoice = (invoice: any) => {
|
|
return invoice?.state === "Gebucht" && !!invoice?.date
|
|
}
|
|
|
|
const loadData = async () => {
|
|
loading.value = true
|
|
|
|
try {
|
|
const [docs, incoming, manual, allocations] = await Promise.all([
|
|
useEntities("createddocuments").select(),
|
|
useEntities("incominginvoices").select(),
|
|
useNuxtApp().$api("/api/banking/manual-bookings"),
|
|
useEntities("statementallocations").select("*, bankstatement(*)")
|
|
])
|
|
|
|
createdDocuments.value = (docs || []).filter(isRelevantOutputDocument)
|
|
incomingInvoices.value = (incoming || []).filter(isRelevantInputInvoice)
|
|
manualBookings.value = (manual as any[]) || []
|
|
bankAllocations.value = (allocations || []).filter((allocation) => isDirectBankAllocation(allocation) || hasCashDiscount(allocation))
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const periods = computed(() => {
|
|
const currentBounds = getTaxEvaluationPeriodBounds(dayjs(), periodType.value)
|
|
|
|
return Array.from({ length: 8 }, (_, index) => {
|
|
const start = shiftTaxEvaluationPeriodStart(currentBounds.start, periodType.value, -index)
|
|
const bounds = getTaxEvaluationPeriodBounds(start, periodType.value)
|
|
|
|
const outputDocs = createdDocuments.value.filter((doc) => {
|
|
const date = dayjs(doc.documentDate)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
|
|
const inputDocs = incomingInvoices.value.filter((invoice) => {
|
|
const date = dayjs(invoice.date)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
|
|
const manualTax = manualBookings.value
|
|
.filter((booking) => {
|
|
const date = dayjs(booking.manualBookingDate)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
.reduce((sum, booking) => {
|
|
const breakdown = getManualBookingTaxBreakdown(booking)
|
|
return {
|
|
outputTax19: sum.outputTax19 + breakdown.outputTax19,
|
|
inputTax19: sum.inputTax19 + breakdown.inputTax19,
|
|
}
|
|
}, { outputTax19: 0, inputTax19: 0 })
|
|
|
|
const output = outputDocs.reduce((sum, doc) => {
|
|
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
|
return {
|
|
net19: sum.net19 + breakdown.net19,
|
|
tax19: sum.tax19 + breakdown.tax19,
|
|
net7: sum.net7 + breakdown.net7,
|
|
tax7: sum.tax7 + breakdown.tax7,
|
|
net0: sum.net0 + breakdown.net0,
|
|
}
|
|
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
|
|
|
|
const input = inputDocs.reduce((sum, invoice) => {
|
|
const breakdown = getIncomingInvoiceTaxBreakdown(invoice)
|
|
return {
|
|
net19: sum.net19 + breakdown.net19,
|
|
tax19: sum.tax19 + breakdown.tax19,
|
|
net7: sum.net7 + breakdown.net7,
|
|
tax7: sum.tax7 + breakdown.tax7,
|
|
net0: sum.net0 + breakdown.net0,
|
|
}
|
|
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
|
|
|
|
const directBookings = bankAllocations.value.filter((allocation) => {
|
|
const date = dayjs(allocation.bankstatement?.date || allocation.bankstatement?.valueDate)
|
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day")
|
|
})
|
|
directBookings.forEach((allocation) => {
|
|
const breakdown = getBankAllocationTaxBreakdown(allocation)
|
|
for (const key of ["net19", "tax19", "net7", "tax7", "net0"] as const) {
|
|
output[key] += breakdown.output[key]
|
|
input[key] += breakdown.input[key]
|
|
}
|
|
})
|
|
|
|
output.tax19 = Number((output.tax19 + manualTax.outputTax19).toFixed(2))
|
|
input.tax19 = Number((input.tax19 + manualTax.inputTax19).toFixed(2))
|
|
|
|
const outputTax = Number((output.tax19 + output.tax7).toFixed(2))
|
|
const inputTax = Number((input.tax19 + input.tax7).toFixed(2))
|
|
const balance = Number((outputTax - inputTax).toFixed(2))
|
|
|
|
return {
|
|
key: bounds.start.format("YYYY-MM-DD"),
|
|
label: formatTaxEvaluationPeriodLabel(bounds.start, periodType.value),
|
|
range: formatTaxEvaluationPeriodRange(bounds.start, periodType.value),
|
|
isCurrent: index === 0,
|
|
outputTax,
|
|
inputTax,
|
|
balance,
|
|
output,
|
|
input,
|
|
outputCount: outputDocs.length,
|
|
inputCount: inputDocs.length,
|
|
bankCount: directBookings.length,
|
|
}
|
|
})
|
|
})
|
|
|
|
const currentPeriod = computed(() => periods.value[0] || null)
|
|
|
|
const columns = [
|
|
{ key: "label", label: "Zeitraum" },
|
|
{ key: "range", label: "Datumsbereich" },
|
|
{ key: "outputTax", label: "Umsatzsteuer" },
|
|
{ key: "inputTax", label: "Vorsteuer" },
|
|
{ key: "balance", label: "Ergebnis" },
|
|
{ key: "documents", label: "Belege" },
|
|
]
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar title="USt-Auswertung">
|
|
<template #right>
|
|
<UButton
|
|
icon="i-heroicons-arrow-path"
|
|
variant="outline"
|
|
@click="loadData"
|
|
:loading="loading"
|
|
>
|
|
Aktualisieren
|
|
</UButton>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
|
|
<UDashboardPanelContent class="p-4 md:p-6">
|
|
<div class="mb-6 flex flex-col gap-2">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Aktueller Zeitraum: {{ currentPeriod?.label }}
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
Intervall: {{ periodType === "monthly" ? "monatlich" : periodType === "quarterly" ? "quartalsweise" : "jährlich" }}.
|
|
Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen, gebuchte Eingangsbelege,
|
|
direkte Bankbuchungen mit Umsatzsteuer oder Vorsteuer sowie manuelle Buchungen auf SKR03 1576 und 1776.
|
|
</p>
|
|
<p v-if="currentPeriod" class="text-sm text-gray-500 dark:text-gray-400">
|
|
{{ currentPeriod.range }}
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="currentPeriod" class="grid gap-4 md:grid-cols-3">
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Berechnete USt</div>
|
|
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
{{ formatCurrency(currentPeriod.outputTax) }}
|
|
</div>
|
|
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
|
19%: {{ formatCurrency(currentPeriod.output.tax19) }} | 7%: {{ formatCurrency(currentPeriod.output.tax7) }}
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Vorsteuer</div>
|
|
<div class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
|
{{ formatCurrency(currentPeriod.inputTax) }}
|
|
</div>
|
|
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
|
19%: {{ formatCurrency(currentPeriod.input.tax19) }} | 7%: {{ formatCurrency(currentPeriod.input.tax7) }}
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400">Verrechnetes Ergebnis</div>
|
|
<div
|
|
class="mt-2 text-2xl font-semibold"
|
|
:class="currentPeriod.balance >= 0 ? 'text-amber-600 dark:text-amber-400' : 'text-emerald-600 dark:text-emerald-400'"
|
|
>
|
|
{{ formatCurrency(currentPeriod.balance) }}
|
|
</div>
|
|
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
|
{{ currentPeriod.outputCount }} Ausgangsbelege | {{ currentPeriod.inputCount }} Eingangsbelege | {{ currentPeriod.bankCount }} direkte Bankbuchungen
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<div v-if="currentPeriod" class="mt-6 grid gap-4 xl:grid-cols-2">
|
|
<UCard>
|
|
<template #header>
|
|
<div class="font-semibold">Umsatzsteuer aus Belegen und Bankbuchungen</div>
|
|
</template>
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.net19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>USt 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.tax19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.net7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>USt 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.tax7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 0%</span>
|
|
<span>{{ formatCurrency(currentPeriod.output.net0) }}</span>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<UCard>
|
|
<template #header>
|
|
<div class="font-semibold">Vorsteuer aus Belegen und Bankbuchungen</div>
|
|
</template>
|
|
<div class="space-y-3 text-sm">
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.net19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Vorsteuer 19%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.tax19) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.net7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Vorsteuer 7%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.tax7) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span>Netto 0%</span>
|
|
<span>{{ formatCurrency(currentPeriod.input.net0) }}</span>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<UCard class="mt-6">
|
|
<template #header>
|
|
<div class="font-semibold">Aktueller und vorherige Zeiträume</div>
|
|
</template>
|
|
|
|
<UTable
|
|
:columns="normalizeTableColumns(columns)"
|
|
:data="periods"
|
|
:loading="loading"
|
|
>
|
|
<template #label-cell="{ row }">
|
|
<div class="flex items-center gap-2">
|
|
<span>{{ row.original.label }}</span>
|
|
<UBadge v-if="row.original.isCurrent" color="primary" variant="soft">Aktuell</UBadge>
|
|
</div>
|
|
</template>
|
|
|
|
<template #outputTax-cell="{ row }">
|
|
{{ formatCurrency(row.original.outputTax) }}
|
|
</template>
|
|
|
|
<template #inputTax-cell="{ row }">
|
|
{{ formatCurrency(row.original.inputTax) }}
|
|
</template>
|
|
|
|
<template #balance-cell="{ row }">
|
|
<span :class="row.original.balance >= 0 ? 'text-amber-600 dark:text-amber-400 font-medium' : 'text-emerald-600 dark:text-emerald-400 font-medium'">
|
|
{{ formatCurrency(row.original.balance) }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #documents-cell="{ row }">
|
|
{{ row.original.outputCount }} / {{ row.original.inputCount }} · {{ row.original.bankCount }} Bankbuchungen
|
|
</template>
|
|
<template #empty>
|
|
<TableEmptyState label="Keine Daten für die USt-Auswertung vorhanden" icon="i-heroicons-calculator" />
|
|
</template>
|
|
</UTable>
|
|
</UCard>
|
|
</UDashboardPanelContent>
|
|
</template>
|