feat(banking): add cost centres and VAT to account allocations

This commit is contained in:
2026-09-23 18:08:09 +02:00
parent 847a616285
commit cbec5b058d
21 changed files with 517 additions and 42 deletions

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import dayjs from "dayjs"
import { getBankAllocationAmounts, getBankAllocationTaxBreakdown } from "~/utils/bankAllocationTax"
import {
getCreatedDocumentTaxBreakdown,
getIncomingInvoiceTaxBreakdown
@@ -201,7 +202,7 @@ const expenseNetTotal = computed(() => {
const expenseGrossTotal = computed(() => {
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => sum + getIncomingInvoiceImmediateExpenseGross(invoice), 0)
const directAccountExpenses = getStatementAccountExpenseTotal(filteredStatementAllocations.value)
const directAccountExpenses = getStatementAccountExpenseTotal(filteredStatementAllocations.value, true)
const depreciations = depreciationTotal.value
@@ -231,6 +232,13 @@ const taxSummary = computed(() => {
}
}, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 })
filteredStatementAllocations.value.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]
}
})
const outputTax = Number((output.tax19 + output.tax7).toFixed(2))
const inputTax = Number((input.tax19 + input.tax7).toFixed(2))
@@ -303,13 +311,9 @@ const accountRows = computed(() => {
const debitBookings = filteredStatementAllocations.value
.filter((allocation) => sameId(allocation.account?.id || allocation.account, account.id))
.map((allocation) => {
const amount = Number(allocation.amount || 0)
return {
type: "statementallocation",
amountNet: amount,
amountTax: 0,
amountGross: amount
...getBankAllocationAmounts(allocation)
}
})

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { getBankAllocationAmounts } from "~/utils/bankAllocationTax"
import dayjs from "dayjs"
import {
DEPRECIATION_METHOD_ITEMS,
@@ -114,7 +115,7 @@ const depreciationAssets = computed(() => {
if (!isDepreciationBookingMode(allocation?.bookingMode)) return null
const status = getAssetDepreciationStatus({
amount: Math.abs(Number(allocation.amount || 0)),
amount: Math.abs(getBankAllocationAmounts(allocation).amountNet),
depreciationMonths: allocation.depreciationMonths,
depreciationStartDate: allocation.depreciationStartDate || allocation.created_at,
depreciationMethod: allocation.depreciationMethod,
@@ -135,7 +136,7 @@ const depreciationAssets = computed(() => {
months: Number(allocation.depreciationMonths || 0),
startDate: allocation.depreciationStartDate || allocation.created_at,
residualValue: Number(allocation.residualValue || 0),
originalValue: Math.abs(Number(allocation.amount || 0)),
originalValue: Math.abs(getBankAllocationAmounts(allocation).amountNet),
currentPeriodAmount: Number(currentPeriodRow?.amount || 0),
depreciated: status.depreciated,
remaining: status.remaining,

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import dayjs from "dayjs"
import { getBankAllocationTaxBreakdown, isDirectBankAllocation } from "~/utils/bankAllocationTax"
import customParseFormat from "dayjs/plugin/customParseFormat"
import {
formatTaxEvaluationPeriodLabel,
@@ -20,6 +21,7 @@ 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))
@@ -42,15 +44,17 @@ const loadData = async () => {
loading.value = true
try {
const [docs, incoming, manual] = await Promise.all([
const [docs, incoming, manual, allocations] = await Promise.all([
useEntities("createddocuments").select(),
useEntities("incominginvoices").select(),
useNuxtApp().$api("/api/banking/manual-bookings")
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(isDirectBankAllocation)
} finally {
loading.value = false
}
@@ -108,6 +112,18 @@ const periods = computed(() => {
}
}, { 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))
@@ -127,6 +143,7 @@ const periods = computed(() => {
input,
outputCount: outputDocs.length,
inputCount: inputDocs.length,
bankCount: directBookings.length,
}
})
})
@@ -136,7 +153,7 @@ const currentPeriod = computed(() => periods.value[0] || null)
const columns = [
{ key: "label", label: "Zeitraum" },
{ key: "range", label: "Datumsbereich" },
{ key: "outputTax", label: "USt Rechnungen" },
{ key: "outputTax", label: "Umsatzsteuer" },
{ key: "inputTax", label: "Vorsteuer" },
{ key: "balance", label: "Ergebnis" },
{ key: "documents", label: "Belege" },
@@ -166,7 +183,8 @@ onMounted(loadData)
</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 sowie manuelle Buchungen auf SKR03 1576 und 1776.
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 }}
@@ -203,7 +221,7 @@ onMounted(loadData)
{{ formatCurrency(currentPeriod.balance) }}
</div>
<div class="mt-3 text-sm text-gray-500 dark:text-gray-400">
{{ currentPeriod.outputCount }} Ausgangsbelege | {{ currentPeriod.inputCount }} Eingangsbelege
{{ currentPeriod.outputCount }} Ausgangsbelege | {{ currentPeriod.inputCount }} Eingangsbelege | {{ currentPeriod.bankCount }} direkte Bankbuchungen
</div>
</UCard>
</div>
@@ -211,7 +229,7 @@ onMounted(loadData)
<div v-if="currentPeriod" class="mt-6 grid gap-4 xl:grid-cols-2">
<UCard>
<template #header>
<div class="font-semibold">Ausgangsrechnungen</div>
<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">
@@ -239,7 +257,7 @@ onMounted(loadData)
<UCard>
<template #header>
<div class="font-semibold">Eingangsbelege</div>
<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">
@@ -298,7 +316,7 @@ onMounted(loadData)
</template>
<template #documents-cell="{ row }">
{{ row.original.outputCount }} / {{ row.original.inputCount }}
{{ 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" />

View File

@@ -1,5 +1,6 @@
<script setup>
import dayjs from "dayjs";
import { BANK_ALLOCATION_TAX_ITEMS, getBankAllocationAmounts } from "~/utils/bankAllocationTax"
import {
DEPRECIATION_METHOD_ITEMS,
EXPENSE_BOOKING_MODE_ITEMS,
@@ -36,6 +37,7 @@ const createddocuments = ref([])
const incominginvoices = ref([])
const accounts = ref([])
const ownaccounts = ref([])
const costcentres = ref([])
const loading = ref(true)
const loadingDocuments = ref(true)
@@ -76,15 +78,17 @@ const setup = async () => {
useEntities("vendors").select()
])
const [statement, accountItems] = await Promise.all([
const [statement, accountItems, costCentreItems] = await Promise.all([
route.params.id
? useEntities("bankstatements").selectSingle(route.params.id, "*, statementallocations(*, cd_id(*), ii_id(*))")
: Promise.resolve(itemInfo.value),
useEntities("accounts").selectSpecial("*", "number", true)
useEntities("accounts").selectSpecial("*", "number", true),
useEntities("costcentres").select()
])
itemInfo.value = statement
accounts.value = accountItems
costcentres.value = costCentreItems
if (itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
manualAllocationSum.value = calculateOpenSum.value
@@ -164,6 +168,21 @@ const selectAccount = (id) => {
const manualAllocationSum = ref(itemInfo.value.amount || 0)
const allocationDescription = ref("")
const allocationCostcentre = ref(null)
const allocationTaxKey = ref(null)
const allocationAmounts = computed(() => getBankAllocationAmounts({
bankstatement: itemInfo.value.id,
account: accountToSave.value,
ownaccount: ownAccountToSave.value,
amount: manualAllocationSum.value,
datevTaxKey: allocationTaxKey.value,
}))
const getCostCentreLabel = (id) => {
const centre = costcentres.value.find(item => item.id === id)
return centre ? `${centre.number} - ${centre.name}` : id
}
const getAllocationTaxLabel = (item) => BANK_ALLOCATION_TAX_ITEMS.find(tax => tax.value === item.datevTaxKey)?.label
|| (item.datevTaxKey ? `Steuerschlüssel ${item.datevTaxKey}` : "")
const allocationBookingMode = ref("expense")
const allocationDepreciationMonths = ref(36)
const allocationDepreciationStartDate = ref(dayjs().format("YYYY-MM-DD"))
@@ -179,6 +198,10 @@ const isDepreciationAllocation = computed(() => isDepreciationBookingMode(alloca
const buildAllocationPayload = (payload) => {
const base = {
...payload,
...((payload.account || payload.ownaccount) ? {
costcentre: allocationCostcentre.value || null,
datevTaxKey: allocationTaxKey.value || null,
} : {}),
bookingMode: normalizeExpenseBookingMode(allocationBookingMode.value)
}
@@ -232,6 +255,8 @@ const saveAllocation = async (allocation) => {
vendorAccountToSave.value = null
customerAccountToSave.value = null
ownAccountToSave.value = null
allocationCostcentre.value = null
allocationTaxKey.value = null
allocationBookingMode.value = "expense"
allocationDepreciationMonths.value = 36
allocationDepreciationStartDate.value = dayjs(itemInfo.value?.date || new Date()).format("YYYY-MM-DD")
@@ -696,6 +721,11 @@ setup()
{{ getAllocationLabel(item) }}
</div>
<div class="text-xs text-gray-500 mt-0.5" v-if="item.description">{{ item.description }}</div>
<div v-if="item.costcentre" class="text-xs text-gray-500 mt-1">Kostenstelle: {{ getCostCentreLabel(item.costcentre) }}</div>
<div v-if="item.datevTaxKey" class="text-xs text-gray-500 mt-1">
{{ getAllocationTaxLabel(item) }} · Netto {{ displayCurrency(getBankAllocationAmounts(item).amountNet) }}
· Steuer {{ displayCurrency(getBankAllocationAmounts(item).amountTax) }}
</div>
<div class="mt-1 flex flex-wrap gap-1" v-if="item.bookingMode && item.bookingMode !== 'expense'">
<UBadge size="xs" color="warning" variant="soft">{{ getAllocationBookingModeLabel(item) }}</UBadge>
<UBadge size="xs" color="neutral" variant="soft" v-if="item.depreciationGroup">{{ item.depreciationGroup }}</UBadge>
@@ -747,7 +777,7 @@ setup()
class="p-4 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 shadow-sm shrink-0 z-10">
<div class="grid grid-cols-12 gap-4 items-end">
<div class="col-span-12 md:col-span-3">
<UFormField label="Betrag" size="sm">
<UFormField label="Bruttobetrag" size="sm">
<UInput v-model="manualAllocationSum" type="number" step="0.01">
<template #trailing><span class="text-gray-500 text-xs">EUR</span></template>
</UInput>
@@ -778,7 +808,7 @@ setup()
<UButton
icon="i-heroicons-check"
label="Buchen"
:disabled="!accountToSave"
:disabled="!accountToSave || !allocationTaxKey"
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, account: accountToSave, description: allocationDescription })"
/>
</div>
@@ -793,6 +823,28 @@ setup()
</div>
</div>
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
<UFormField label="Umsatzsteuer / Vorsteuer" size="sm">
<USelectMenu v-model="allocationTaxKey" :items="BANK_ALLOCATION_TAX_ITEMS" value-key="value" label-key="label"
placeholder="Steuer auswählen …" class="w-full" />
</UFormField>
<UFormField label="Kostenstelle (optional)" size="sm">
<div class="flex gap-1">
<USelectMenu v-model="allocationCostcentre" :items="costcentres" value-key="id" label-key="name"
:filter-fields="['number', 'name']" placeholder="Keine Kostenstelle" class="w-full">
<template #default>{{ allocationCostcentre ? getCostCentreLabel(allocationCostcentre) : 'Keine Kostenstelle' }}</template>
<template #item-label="{ item }">{{ item.number }} - {{ item.name }}</template>
</USelectMenu>
<UButton v-if="allocationCostcentre" icon="i-heroicons-x-mark" variant="ghost" aria-label="Kostenstelle entfernen"
@click="allocationCostcentre = null" />
</div>
</UFormField>
<p v-if="allocationTaxKey && (accountToSave || ownAccountToSave)" class="md:col-span-2 text-xs text-gray-500">
Netto {{ displayCurrency(allocationAmounts.amountNet) }} · Steuer {{ displayCurrency(allocationAmounts.amountTax) }}
· Brutto {{ displayCurrency(allocationAmounts.amountGross) }}
</p>
</div>
<div v-if="showMoreWithoutRecipe"
class="mt-4 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
@@ -866,6 +918,7 @@ setup()
</USelectMenu>
<div class="md:col-span-3 flex justify-end">
<UButton size="xs" label="Zuweisen"
:disabled="!!ownAccountToSave && !allocationTaxKey"
@click="saveAllocation({bankstatement: itemInfo.id, amount: manualAllocationSum, ownaccount: ownAccountToSave ? ownAccountToSave : null, customer: customerAccountToSave ? customerAccountToSave : null, vendor: vendorAccountToSave ? vendorAccountToSave : null, description: allocationDescription })"/>
</div>
</div>