feat(banking): add cost centres and VAT to account allocations
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs"
|
||||
import { getBankAllocationAmounts, isDirectBankAllocation } from "~/utils/bankAllocationTax"
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
@@ -12,6 +13,8 @@ const loading = ref(true)
|
||||
const incomingInvoices = ref([])
|
||||
const createddocuments = ref([])
|
||||
const costcentres = ref([])
|
||||
const bankAllocations = ref([])
|
||||
const accounts = ref([])
|
||||
const selectedYear = ref(String(dayjs().year()))
|
||||
const selectedMonth = ref("all")
|
||||
|
||||
@@ -74,8 +77,10 @@ const relevantCostCentreIds = computed(() => {
|
||||
|
||||
const yearItems = computed(() => {
|
||||
const years = [...new Set(
|
||||
incomingInvoices.value
|
||||
.map((invoice) => invoice.date ? String(dayjs(invoice.date).year()) : null)
|
||||
[...incomingInvoices.value.map(invoice => invoice.date),
|
||||
...createddocuments.value.map(document => document.documentDate),
|
||||
...bankAllocations.value.map(allocation => allocation.bankstatement?.date)]
|
||||
.map((date) => date ? String(dayjs(date).year()) : null)
|
||||
.filter(Boolean)
|
||||
)].sort((a, b) => Number(b) - Number(a))
|
||||
|
||||
@@ -183,7 +188,33 @@ const reportRows = computed(() => {
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
return [...incomingRows, ...outgoingRows]
|
||||
const bankRows = bankAllocations.value.flatMap((allocation) => {
|
||||
const statement = allocation.bankstatement
|
||||
const date = dayjs(statement.date || statement.valueDate)
|
||||
if (!date.isValid() || String(date.year()) !== selectedYear.value
|
||||
|| (selectedMonth.value !== "all" && date.month() + 1 !== Number(selectedMonth.value))) return []
|
||||
const centre = costCentreMap.value.get(getCostCentreId(allocation.costcentre))
|
||||
const account = accounts.value.find(account => account.id === (allocation.account?.id || allocation.account)) || allocation.ownaccount
|
||||
const amounts = getBankAllocationAmounts(allocation)
|
||||
// As for incoming documents, expenses are positive and refunds negative.
|
||||
const sign = amounts.kind === "input" || (amounts.kind === "none" && amounts.amountGross < 0) ? -1 : 1
|
||||
return [{
|
||||
id: `bank-${allocation.id}`,
|
||||
sourceLabel: "Bankbuchung",
|
||||
statementId: statement.id,
|
||||
reference: `Bank #${statement.id}`,
|
||||
date: statement.date || statement.valueDate,
|
||||
state: "Gebucht",
|
||||
vendorName: statement.debName || statement.credName || "-",
|
||||
accountLabel: `${account?.number || ""} ${account?.label || account?.name || ""}`.trim() || "-",
|
||||
costCentreName: centre ? `${centre.number} - ${centre.name}` : "-",
|
||||
description: allocation.description || statement.text || "-",
|
||||
amountNet: amounts.amountNet * sign,
|
||||
amountTax: amounts.amountTax * sign,
|
||||
amountGross: amounts.amountGross * sign,
|
||||
}]
|
||||
})
|
||||
return [...incomingRows, ...outgoingRows, ...bankRows]
|
||||
})
|
||||
|
||||
const totals = computed(() => {
|
||||
@@ -212,8 +243,12 @@ const setupPage = async () => {
|
||||
loading.value = true
|
||||
|
||||
costcentres.value = await useEntities("costcentres").select("*", null, false, true)
|
||||
accounts.value = await useEntities("accounts").selectSpecial()
|
||||
const invoices = await useEntities("incominginvoices").select("*, vendor(id,name)")
|
||||
const documents = await useEntities("createddocuments").select("*, customer(id,name)")
|
||||
const allocations = await useEntities("statementallocations").select("*, bankstatement(*), account(*), ownaccount(*)")
|
||||
bankAllocations.value = allocations.filter(allocation => isDirectBankAllocation(allocation)
|
||||
&& relevantCostCentreIds.value.has(getCostCentreId(allocation.costcentre)))
|
||||
|
||||
incomingInvoices.value = invoices.filter((invoice) =>
|
||||
(invoice.accounts || []).some((account) => relevantCostCentreIds.value.has(account.costCentre))
|
||||
@@ -283,7 +318,10 @@ setupPage()
|
||||
class="w-full"
|
||||
>
|
||||
<template #reference-cell="{ row }">
|
||||
<div class="truncate font-medium">{{ row.original.reference }}</div>
|
||||
<NuxtLink v-if="row.original.statementId" :to="`/banking/statements/show/${row.original.statementId}`" class="font-medium text-primary">
|
||||
{{ row.original.reference }}
|
||||
</NuxtLink>
|
||||
<div v-else class="truncate font-medium">{{ row.original.reference }}</div>
|
||||
</template>
|
||||
|
||||
<template #date-cell="{ row }">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs"
|
||||
import { getBankAllocationTaxBreakdown } from "~/utils/bankAllocationTax"
|
||||
import {
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown
|
||||
@@ -125,6 +126,12 @@ const loadSummary = async () => {
|
||||
return sum + breakdown.tax19 + breakdown.tax7
|
||||
}, 0)
|
||||
|
||||
const bankTaxBalance = (allocations || []).reduce((sum: number, allocation: any) => {
|
||||
const date = dayjs(allocation.bankstatement?.date || allocation.bankstatement?.valueDate)
|
||||
if (!date.isValid() || date.isBefore(bounds.start, "day") || date.isAfter(bounds.end, "day")) return sum
|
||||
const { output, input } = getBankAllocationTaxBreakdown(allocation)
|
||||
return sum + output.tax19 + output.tax7 - input.tax19 - input.tax7
|
||||
}, 0)
|
||||
const expenses = invoiceExpenses + directAccountExpenses + depreciations
|
||||
|
||||
summary.value = {
|
||||
@@ -133,7 +140,7 @@ const loadSummary = async () => {
|
||||
expenses: Number(expenses.toFixed(2)),
|
||||
depreciations: Number(depreciations.toFixed(2)),
|
||||
result: Number((income - Math.abs(expenses)).toFixed(2)),
|
||||
taxBalance: Number((outputTax - inputTax).toFixed(2)),
|
||||
taxBalance: Number((outputTax - inputTax + bankTaxBalance).toFixed(2)),
|
||||
incomeCount: outputDocs.length,
|
||||
expenseCount: inputDocs.filter((invoice: any) => getIncomingInvoiceImmediateExpenseNet(invoice) > 0).length + directAccountAllocations.filter((allocation: any) => getStatementAllocationImmediateExpenseAmount(allocation) > 0).length,
|
||||
depreciationCount: depreciationRows.length
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getBankAllocationAmounts } from "../utils/bankAllocationTax"
|
||||
import dayjs from "dayjs"
|
||||
|
||||
export const EXPENSE_BOOKING_MODE_ITEMS = [
|
||||
@@ -308,7 +309,7 @@ export const getStatementAllocationImmediateExpenseImpact = (allocation: any) =>
|
||||
if (allocation?.account === null || allocation?.account === undefined) return 0
|
||||
|
||||
const mode = normalizeExpenseBookingMode(allocation?.bookingMode)
|
||||
const amount = Number(allocation?.amount || 0)
|
||||
const amount = getBankAllocationAmounts(allocation).amountNet
|
||||
if (mode !== "expense") return 0
|
||||
return Number((-amount).toFixed(2))
|
||||
}
|
||||
@@ -317,7 +318,7 @@ export const getStatementAllocationImmediateExpenseAmount = (allocation: any) =>
|
||||
return Math.max(0, getStatementAllocationImmediateExpenseImpact(allocation))
|
||||
}
|
||||
|
||||
export const getStatementAccountExpenseTotal = (allocations: any[] = []) => {
|
||||
export const getStatementAccountExpenseTotal = (allocations: any[] = [], gross = false) => {
|
||||
const balances = new Map<string, number>()
|
||||
const addAmount = (account: any, amount: number) => {
|
||||
const id = account?.id || account
|
||||
@@ -330,7 +331,8 @@ export const getStatementAccountExpenseTotal = (allocations: any[] = []) => {
|
||||
;(allocations || []).forEach((allocation) => {
|
||||
if (normalizeExpenseBookingMode(allocation?.bookingMode) !== "expense") return
|
||||
|
||||
const amount = Number(allocation?.amount || 0)
|
||||
const amounts = getBankAllocationAmounts(allocation)
|
||||
const amount = gross ? amounts.amountGross : amounts.amountNet
|
||||
addAmount(allocation?.account, amount)
|
||||
addAmount(allocation?.contraAccount, -amount)
|
||||
})
|
||||
@@ -344,7 +346,7 @@ export const getStatementAccountExpenseTotal = (allocations: any[] = []) => {
|
||||
|
||||
export const getStatementAllocationDepreciationAmount = (allocation: any, rangeStart: any, rangeEnd: any) => {
|
||||
const mode = normalizeExpenseBookingMode(allocation?.bookingMode)
|
||||
const rawAmount = Number(allocation?.amount || 0)
|
||||
const rawAmount = getBankAllocationAmounts(allocation).amountNet
|
||||
const amount = Math.abs(rawAmount)
|
||||
if (mode === "expense" || rawAmount >= 0 || amount <= 0) return 0
|
||||
|
||||
@@ -360,7 +362,7 @@ export const getStatementAllocationDepreciationAmount = (allocation: any, rangeS
|
||||
}
|
||||
|
||||
export const getStatementAllocationDepreciationRow = (allocation: any, rangeStart: any, rangeEnd: any) => {
|
||||
const totalAmount = Math.abs(Number(allocation?.amount || 0))
|
||||
const totalAmount = Math.abs(getBankAllocationAmounts(allocation).amountNet)
|
||||
const months = Number(allocation?.depreciationMonths || 0)
|
||||
const startDate = allocation?.depreciationStartDate || allocation?.created_at
|
||||
const method = normalizeDepreciationMethod(allocation?.depreciationMethod)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
63
frontend/tests/bank-allocation-tax.test.cjs
Normal file
63
frontend/tests/bank-allocation-tax.test.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
const { test } = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const vm = require('node:vm')
|
||||
const ts = require('typescript')
|
||||
const path = require('node:path')
|
||||
|
||||
function loadModule(file) {
|
||||
const filename = path.resolve(__dirname, file)
|
||||
const module = { exports: {} }
|
||||
const localRequire = id => id.startsWith('.') ? loadModule(path.relative(__dirname, path.resolve(path.dirname(filename), id + '.ts'))) : require(id)
|
||||
vm.runInNewContext(ts.transpile(fs.readFileSync(filename, 'utf8'), { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true }), { module, exports: module.exports, require: localRequire })
|
||||
return module.exports
|
||||
}
|
||||
const { getBankAllocationAmounts, getBankAllocationTaxBreakdown } = loadModule('../utils/bankAllocationTax.ts')
|
||||
const { getStatementAccountExpenseTotal, getStatementAllocationDepreciationAmount } = loadModule('../composables/useDepreciation.ts')
|
||||
const allocation = (amount, datevTaxKey, extra = {}) => ({ bankstatement: { id: 42 }, account: 13, amount, datevTaxKey, ...extra })
|
||||
|
||||
for (const [gross, key, net, tax, side, rate] of [
|
||||
[-119, '9', -100, -19, 'input', 19], [-107, '8', -100, -7, 'input', 7],
|
||||
[119, '3', 100, 19, 'output', 19], [107, '2', 100, 7, 'output', 7],
|
||||
[59.5, '9', 50, 9.5, 'input', 19], [-59.5, '3', -50, -9.5, 'output', 19],
|
||||
]) {
|
||||
test(`gross ${gross} and key ${key} produce correct net, VAT and refund signs`, () => {
|
||||
const item = allocation(gross, key)
|
||||
const amounts = getBankAllocationAmounts(item)
|
||||
assert.equal(amounts.amountGross, gross)
|
||||
assert.equal(amounts.amountNet, net)
|
||||
assert.equal(amounts.amountTax, tax)
|
||||
const result = getBankAllocationTaxBreakdown(item)
|
||||
assert.equal(result[side][`net${rate}`], net * (side === 'input' ? -1 : 1))
|
||||
assert.equal(result[side][`tax${rate}`], tax * (side === 'input' ? -1 : 1))
|
||||
})
|
||||
}
|
||||
test('rounding preserves gross, and tax-free and legacy allocations retain their amounts', () => {
|
||||
const amounts = getBankAllocationAmounts(allocation(-10, '9'))
|
||||
assert.equal(amounts.amountNet, -8.4)
|
||||
assert.equal(amounts.amountTax, -1.6)
|
||||
for (const key of ['40', null]) {
|
||||
const value = getBankAllocationAmounts(allocation(-100, key))
|
||||
assert.equal(value.amountNet, -100)
|
||||
assert.equal(value.amountTax, 0)
|
||||
}
|
||||
})
|
||||
test('invoice settlements, archived bookings and manual bookings are not counted again', () => {
|
||||
for (const extra of [{ incominginvoice: 1 }, { createddocument: 1 }, { archived: true }, { bankstatement: null }]) {
|
||||
const result = getBankAllocationTaxBreakdown(allocation(-119, '9', extra))
|
||||
assert.ok([...Object.values(result.input), ...Object.values(result.output)].every(value => value === 0))
|
||||
}
|
||||
})
|
||||
test('additional accounts carry the same tax breakdown', () => {
|
||||
assert.equal(getBankAllocationAmounts(allocation(-119, '9', { account: null, ownaccount: 'own' })).amountTax, -19)
|
||||
})
|
||||
test('BWA separates net expenses from gross cash flows including partial refunds', () => {
|
||||
const items = [allocation(-119, '9'), allocation(59.5, '9')]
|
||||
assert.equal(getStatementAccountExpenseTotal(items), 50)
|
||||
assert.equal(getStatementAccountExpenseTotal(items, true), 59.5)
|
||||
})
|
||||
test('depreciation uses the net acquisition amount when input VAT is selected', () => {
|
||||
assert.equal(getStatementAllocationDepreciationAmount(allocation(-1428, '9', {
|
||||
bookingMode: 'depreciation_single', depreciationMonths: 12, depreciationStartDate: '2026-01-01',
|
||||
}), '2026-01-01', '2026-01-31'), 100)
|
||||
})
|
||||
43
frontend/utils/bankAllocationTax.ts
Normal file
43
frontend/utils/bankAllocationTax.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export const BANK_ALLOCATION_TAX_ITEMS = [
|
||||
{ value: "9", label: "19 % Vorsteuer", rate: 19, kind: "input" },
|
||||
{ value: "8", label: "7 % Vorsteuer", rate: 7, kind: "input" },
|
||||
{ value: "3", label: "19 % Umsatzsteuer", rate: 19, kind: "output" },
|
||||
{ value: "2", label: "7 % Umsatzsteuer", rate: 7, kind: "output" },
|
||||
{ value: "40", label: "Ohne Umsatzsteuer", rate: 0, kind: "none" },
|
||||
]
|
||||
|
||||
export const isDirectBankAllocation = (allocation: any) => Boolean(
|
||||
allocation?.bankstatement && (allocation.account || allocation.ownaccount)
|
||||
&& !allocation.createddocument && !allocation.incominginvoice
|
||||
&& !allocation.customer && !allocation.vendor && !allocation.archived
|
||||
)
|
||||
|
||||
// The signed bank amount always remains gross. Refunds retain their sign.
|
||||
export function getBankAllocationAmounts(allocation: any) {
|
||||
const amountGross = Number(allocation?.amount || 0)
|
||||
const tax = isDirectBankAllocation(allocation)
|
||||
? BANK_ALLOCATION_TAX_ITEMS.find(item => item.value === String(allocation.datevTaxKey))
|
||||
: undefined
|
||||
const rate = tax?.rate || 0
|
||||
const amountNet = Number((amountGross / (1 + rate / 100)).toFixed(2))
|
||||
const amountTax = Number((amountGross - amountNet).toFixed(2))
|
||||
return { amountGross, amountNet, amountTax, rate, kind: tax?.kind || "none" }
|
||||
}
|
||||
|
||||
export function getBankAllocationTaxBreakdown(allocation: any) {
|
||||
const output = { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 }
|
||||
const input = { ...output }
|
||||
if (!isDirectBankAllocation(allocation)) return { output, input }
|
||||
const { amountNet, amountTax, rate, kind } = getBankAllocationAmounts(allocation)
|
||||
if (kind === "none") return { output, input }
|
||||
const target = kind === "input" ? input : output
|
||||
const sign = kind === "input" ? -1 : 1
|
||||
if (rate === 19) {
|
||||
target.net19 = amountNet * sign
|
||||
target.tax19 = amountTax * sign
|
||||
} else if (rate === 7) {
|
||||
target.net7 = amountNet * sign
|
||||
target.tax7 = amountTax * sign
|
||||
}
|
||||
return { output, input }
|
||||
}
|
||||
Reference in New Issue
Block a user