Added UST Auswertung
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
289
frontend/pages/accounting/tax.vue
Normal file
289
frontend/pages/accounting/tax.vue
Normal 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>
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user