Added UST Auswertung

This commit is contained in:
2026-03-21 17:44:25 +01:00
parent 6f77bccd85
commit 23c4d21f44
5 changed files with 327 additions and 0 deletions

View File

@@ -161,6 +161,10 @@ export const tenants = pgTable(
.notNull() .notNull()
.default(14), .default(14),
taxEvaluationPeriod: text("taxEvaluationPeriod")
.notNull()
.default("monthly"),
dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]), dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]),
dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(), dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(),

View File

@@ -57,6 +57,7 @@ export default async function meRoutes(server: FastifyInstance) {
businessInfo: tenants.businessInfo, businessInfo: tenants.businessInfo,
numberRanges: tenants.numberRanges, numberRanges: tenants.numberRanges,
accountChart: tenants.accountChart, accountChart: tenants.accountChart,
taxEvaluationPeriod: tenants.taxEvaluationPeriod,
dokuboxkey: tenants.dokuboxkey, dokuboxkey: tenants.dokuboxkey,
standardEmailForInvoices: tenants.standardEmailForInvoices, standardEmailForInvoices: tenants.standardEmailForInvoices,
standardPaymentDays: tenants.standardPaymentDays, standardPaymentDays: tenants.standardPaymentDays,

View File

@@ -0,0 +1,289 @@
<script setup lang="ts">
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
import {
formatTaxEvaluationPeriodLabel,
formatTaxEvaluationPeriodRange,
getCreatedDocumentTaxBreakdown,
getIncomingInvoiceTaxBreakdown,
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 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] = await Promise.all([
useEntities("createddocuments").select(),
useEntities("incominginvoices").select()
])
createdDocuments.value = (docs || []).filter(isRelevantOutputDocument)
incomingInvoices.value = (incoming || []).filter(isRelevantInputInvoice)
} 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 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 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,
}
})
})
const currentPeriod = computed(() => periods.value[0] || null)
const columns = [
{ key: "label", label: "Zeitraum" },
{ key: "range", label: "Datumsbereich" },
{ key: "outputTax", label: "USt Rechnungen" },
{ key: "inputTax", label: "Vorsteuer" },
{ key: "balance", label: "Ergebnis" },
{ key: "documents", label: "Belege" },
]
onMounted(loadData)
</script>
<template>
<div>
<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 sowie gebuchte Eingangsbelege mit Datum.
</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
</div>
</UCard>
</div>
<div v-if="currentPeriod" class="mt-6 grid gap-4 xl:grid-cols-2">
<UCard>
<template #header>
<div class="font-semibold">Ausgangsrechnungen</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">Eingangsbelege</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="columns"
:rows="periods"
:loading="loading"
:empty-state="{ icon: 'i-heroicons-calculator', label: 'Keine Daten für die USt-Auswertung vorhanden' }"
>
<template #label-data="{ row }">
<div class="flex items-center gap-2">
<span>{{ row.label }}</span>
<UBadge v-if="row.isCurrent" color="primary" variant="soft">Aktuell</UBadge>
</div>
</template>
<template #outputTax-data="{ row }">
{{ formatCurrency(row.outputTax) }}
</template>
<template #inputTax-data="{ row }">
{{ formatCurrency(row.inputTax) }}
</template>
<template #balance-data="{ row }">
<span :class="row.balance >= 0 ? 'text-amber-600 dark:text-amber-400 font-medium' : 'text-emerald-600 dark:text-emerald-400 font-medium'">
{{ formatCurrency(row.balance) }}
</span>
</template>
<template #documents-data="{ row }">
{{ row.outputCount }} / {{ row.inputCount }}
</template>
</UTable>
</UCard>
</UDashboardPanelContent>
</div>
</template>

View File

@@ -7,6 +7,7 @@ import DisplayOpenBalances from "~/components/displayOpenBalances.vue"
import DisplayBankaccounts from "~/components/displayBankaccounts.vue" import DisplayBankaccounts from "~/components/displayBankaccounts.vue"
import DisplayProjectsInPhases from "~/components/displayProjectsInPhases.vue" import DisplayProjectsInPhases from "~/components/displayProjectsInPhases.vue"
import DisplayOpenTasks from "~/components/displayOpenTasks.vue" import DisplayOpenTasks from "~/components/displayOpenTasks.vue"
import DisplayTaxSummary from "~/components/displayTaxSummary.vue"
setPageLayout("default") setPageLayout("default")
@@ -68,6 +69,15 @@ const DASHBOARD_WIDGETS = [
defaultLayout: { x: 0, y: 7, w: 4, h: 3 }, defaultLayout: { x: 0, y: 7, w: 4, h: 3 },
minW: 3, minW: 3,
minH: 3 minH: 3
},
{
id: "tax-summary",
title: "USt aktuell",
description: "USt, Vorsteuer und Saldo des aktuellen Zeitraums",
component: markRaw(DisplayTaxSummary),
defaultLayout: { x: 4, y: 7, w: 4, h: 3 },
minW: 3,
minH: 3
} }
] ]

View File

@@ -1,4 +1,8 @@
<script setup> <script setup>
import {
TAX_EVALUATION_PERIOD_OPTIONS,
normalizeTaxEvaluationPeriod
} from "~/composables/useTaxEvaluation"
const auth = useAuthStore() const auth = useAuthStore()
const defaultFeatures = { const defaultFeatures = {
@@ -119,6 +123,7 @@ const setupPage = async () => {
const features = ref({ ...defaultFeatures, ...(auth.activeTenantData?.features || {}) }) const features = ref({ ...defaultFeatures, ...(auth.activeTenantData?.features || {}) })
const businessInfo = ref(auth.activeTenantData.businessInfo) const businessInfo = ref(auth.activeTenantData.businessInfo)
const accountChart = ref(auth.activeTenantData.accountChart || "skr03") const accountChart = ref(auth.activeTenantData.accountChart || "skr03")
const taxEvaluationPeriod = ref(normalizeTaxEvaluationPeriod(auth.activeTenantData.taxEvaluationPeriod))
const accountChartOptions = [ const accountChartOptions = [
{ label: "SKR 03", value: "skr03" }, { label: "SKR 03", value: "skr03" },
{ label: "Verein", value: "verein" } { label: "Verein", value: "verein" }
@@ -137,6 +142,7 @@ const updateTenant = async (newData) => {
itemInfo.value = res itemInfo.value = res
auth.activeTenantData = res auth.activeTenantData = res
features.value = { ...defaultFeatures, ...(res?.features || {}) } features.value = { ...defaultFeatures, ...(res?.features || {}) }
taxEvaluationPeriod.value = normalizeTaxEvaluationPeriod(res?.taxEvaluationPeriod)
} }
} }
const saveFeatures = async () => { const saveFeatures = async () => {
@@ -226,6 +232,23 @@ setupPage()
> >
Kontenrahmen speichern Kontenrahmen speichern
</UButton> </UButton>
<UFormGroup
label="USt-Auswertung:"
class="mt-6"
>
<USelectMenu
v-model="taxEvaluationPeriod"
:options="TAX_EVALUATION_PERIOD_OPTIONS"
option-attribute="label"
value-attribute="value"
/>
</UFormGroup>
<UButton
class="mt-3"
@click="updateTenant({taxEvaluationPeriod: taxEvaluationPeriod})"
>
Zeitraum speichern
</UButton>
</UForm> </UForm>
</UCard> </UCard>