Added Abschreibungen
This commit is contained in:
@@ -4,6 +4,15 @@ import {
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown
|
||||
} from "~/composables/useTaxEvaluation"
|
||||
import {
|
||||
getIncomingInvoiceDepreciationRows,
|
||||
getIncomingInvoiceImmediateExpenseGross,
|
||||
getIncomingInvoiceImmediateExpenseNet,
|
||||
isDepreciationBookingMode,
|
||||
normalizeIncomingInvoiceAccount,
|
||||
getStatementAllocationDepreciationRow,
|
||||
getStatementAllocationImmediateExpenseAmount
|
||||
} from "~/composables/useDepreciation"
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -51,6 +60,14 @@ const ownAccountColumns = [
|
||||
{ accessorKey: "bookings", header: "Buchungen" }
|
||||
]
|
||||
|
||||
const depreciationColumns = [
|
||||
{ accessorKey: "label", header: "Abschreibung" },
|
||||
{ accessorKey: "groupLabel", header: "Gruppe" },
|
||||
{ accessorKey: "modeLabel", header: "Art" },
|
||||
{ accessorKey: "amount", header: "Betrag" },
|
||||
{ accessorKey: "bookings", header: "Positionen" }
|
||||
]
|
||||
|
||||
const isRelevantOutputDocument = (doc: any) => {
|
||||
return doc?.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)
|
||||
}
|
||||
@@ -97,16 +114,6 @@ const computeDocumentNet = (doc: any) => {
|
||||
}, 0).toFixed(2))
|
||||
}
|
||||
|
||||
const computeIncomingInvoiceGross = (invoice: any) => {
|
||||
return Number((invoice?.accounts || []).reduce((sum: number, account: any) => {
|
||||
const amountNet = Number(account?.amountNet || 0)
|
||||
const amountTax = Number(account?.amountTax || 0)
|
||||
const amountGross = Number(account?.amountGross)
|
||||
|
||||
return sum + (Number.isFinite(amountGross) ? amountGross : amountNet + amountTax)
|
||||
}, 0).toFixed(2))
|
||||
}
|
||||
|
||||
const yearItems = computed(() => {
|
||||
const years = new Set<string>([String(dayjs().year())])
|
||||
|
||||
@@ -152,32 +159,46 @@ const filteredAccountStatementAllocations = computed(() => {
|
||||
return filteredStatementAllocations.value.filter((allocation) => allocation.account !== null && allocation.account !== undefined)
|
||||
})
|
||||
|
||||
const selectedPeriodBounds = computed(() => {
|
||||
const start = selectedMonth.value === "all"
|
||||
? dayjs(`${selectedYear.value}-01-01`).startOf("day")
|
||||
: dayjs(`${selectedYear.value}-${String(selectedMonth.value).padStart(2, "0")}-01`).startOf("month")
|
||||
|
||||
const end = selectedMonth.value === "all"
|
||||
? dayjs(`${selectedYear.value}-12-31`).endOf("day")
|
||||
: start.endOf("month")
|
||||
|
||||
return { start, end }
|
||||
})
|
||||
|
||||
const incomeTotal = computed(() => {
|
||||
return Number(filteredDocuments.value.reduce((sum, doc) => sum + computeDocumentNet(doc), 0).toFixed(2))
|
||||
})
|
||||
|
||||
const expenseNetTotal = computed(() => {
|
||||
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => {
|
||||
return sum + (invoice.accounts || []).reduce((accountSum: number, account: any) => accountSum + Number(account.amountNet || 0), 0)
|
||||
return sum + getIncomingInvoiceImmediateExpenseNet(invoice)
|
||||
}, 0)
|
||||
|
||||
const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
|
||||
const amount = Number(allocation.amount || 0)
|
||||
return amount < 0 ? sum + Math.abs(amount) : sum
|
||||
return sum + getStatementAllocationImmediateExpenseAmount(allocation)
|
||||
}, 0)
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses).toFixed(2))
|
||||
const depreciations = depreciationTotal.value
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses + depreciations).toFixed(2))
|
||||
})
|
||||
|
||||
const expenseGrossTotal = computed(() => {
|
||||
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => sum + computeIncomingInvoiceGross(invoice), 0)
|
||||
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => sum + getIncomingInvoiceImmediateExpenseGross(invoice), 0)
|
||||
|
||||
const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
|
||||
const amount = Number(allocation.amount || 0)
|
||||
return amount < 0 ? sum + Math.abs(amount) : sum
|
||||
return sum + getStatementAllocationImmediateExpenseAmount(allocation)
|
||||
}, 0)
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses).toFixed(2))
|
||||
const depreciations = depreciationTotal.value
|
||||
|
||||
return Number((invoiceExpenses + directAccountExpenses + depreciations).toFixed(2))
|
||||
})
|
||||
|
||||
const taxSummary = computed(() => {
|
||||
@@ -221,7 +242,42 @@ const operatingResult = computed(() => {
|
||||
|
||||
const incomeDocumentCount = computed(() => filteredDocuments.value.length)
|
||||
const expenseDocumentCount = computed(() => {
|
||||
return filteredIncomingInvoices.value.length + filteredAccountStatementAllocations.value.length
|
||||
const directExpenseCount = filteredAccountStatementAllocations.value.filter((allocation) => getStatementAllocationImmediateExpenseAmount(allocation) > 0).length
|
||||
return filteredIncomingInvoices.value.filter((invoice) => getIncomingInvoiceImmediateExpenseNet(invoice) > 0).length + directExpenseCount
|
||||
})
|
||||
|
||||
const depreciationRows = computed(() => {
|
||||
const invoiceRows = filteredIncomingInvoices.value.flatMap((invoice) => getIncomingInvoiceDepreciationRows(invoice, selectedPeriodBounds.value.start, selectedPeriodBounds.value.end))
|
||||
|
||||
const allocationRows = filteredStatementAllocations.value
|
||||
.map((allocation) => getStatementAllocationDepreciationRow(allocation, selectedPeriodBounds.value.start, selectedPeriodBounds.value.end))
|
||||
.filter(Boolean)
|
||||
|
||||
const grouped = new Map<string, any>()
|
||||
|
||||
;[...invoiceRows, ...allocationRows].forEach((row: any) => {
|
||||
const key = row.group || `${row.mode}:${row.label}`
|
||||
const current = grouped.get(key) || {
|
||||
id: key,
|
||||
label: row.group || row.label,
|
||||
groupLabel: row.group || "-",
|
||||
modeLabel: row.mode === "depreciation_bundle" ? "Sammelposten" : "Einzeln",
|
||||
amount: 0,
|
||||
bookings: 0
|
||||
}
|
||||
|
||||
current.amount += Number(row.amount || 0)
|
||||
current.bookings += 1
|
||||
grouped.set(key, current)
|
||||
})
|
||||
|
||||
return Array.from(grouped.values())
|
||||
.map((row) => ({ ...row, amount: Number(row.amount.toFixed(2)) }))
|
||||
.sort((left, right) => Number(right.amount) - Number(left.amount))
|
||||
})
|
||||
|
||||
const depreciationTotal = computed(() => {
|
||||
return Number(depreciationRows.value.reduce((sum, row) => sum + Number(row.amount || 0), 0).toFixed(2))
|
||||
})
|
||||
|
||||
const accountRows = computed(() => {
|
||||
@@ -229,6 +285,7 @@ const accountRows = computed(() => {
|
||||
.map((account) => {
|
||||
const invoiceBookings = filteredIncomingInvoices.value.flatMap((invoice) => {
|
||||
return (invoice.accounts || [])
|
||||
.filter((invoiceAccount: any) => !isDepreciationBookingMode(normalizeIncomingInvoiceAccount(invoiceAccount, invoice?.date).bookingMode))
|
||||
.filter((invoiceAccount: any) => sameId(invoiceAccount.account?.id || invoiceAccount.account, account.id))
|
||||
.map((invoiceAccount: any) => ({
|
||||
type: "incominginvoice",
|
||||
@@ -241,6 +298,7 @@ const accountRows = computed(() => {
|
||||
})
|
||||
|
||||
const directBookings = filteredAccountStatementAllocations.value
|
||||
.filter((allocation) => getStatementAllocationImmediateExpenseAmount(allocation) > 0)
|
||||
.filter((allocation) => sameId(allocation.account?.id || allocation.account, account.id))
|
||||
.map((allocation) => {
|
||||
const amount = Number(allocation.amount || 0)
|
||||
@@ -387,7 +445,7 @@ onMounted(setupPage)
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div class="grid min-w-0 gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen netto</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ useCurrency(incomeTotal) }}</div>
|
||||
@@ -398,12 +456,20 @@ onMounted(setupPage)
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Ausgaben netto</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ useCurrency(expenseNetTotal) }}</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ useCurrency(expenseNetTotal) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Brutto: {{ useCurrency(expenseGrossTotal) }}
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Abschreibungen</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-amber-600 dark:text-amber-400">{{ useCurrency(depreciationTotal) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ depreciationRows.length }} periodisierte Buchungen
|
||||
</div>
|
||||
</UCard>
|
||||
|
||||
<UCard class="min-w-0">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen Belege</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ incomeDocumentCount }}</div>
|
||||
@@ -503,10 +569,10 @@ onMounted(setupPage)
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div class="grid min-w-0 gap-4 xl:grid-cols-2">
|
||||
<UCard class="min-w-0">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="grid min-w-0 gap-4 xl:grid-cols-2">
|
||||
<UCard class="min-w-0">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="font-semibold">Buchungskonten</span>
|
||||
<UBadge color="neutral" variant="soft">{{ accountRows.length }}</UBadge>
|
||||
</div>
|
||||
@@ -594,7 +660,26 @@ onMounted(setupPage)
|
||||
</template>
|
||||
</UTable>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<UCard class="min-w-0" v-if="depreciationRows.length > 0">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="font-semibold">Abschreibungen</span>
|
||||
<UBadge color="warning" variant="soft">{{ depreciationRows.length }}</UBadge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UTable
|
||||
:data="depreciationRows"
|
||||
:columns="normalizeTableColumns(depreciationColumns)"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #amount-cell="{ row }">
|
||||
<div class="text-right text-amber-600 dark:text-amber-400 tabular-nums">{{ useCurrency(row.original.amount) }}</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</UCard>
|
||||
</div>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
|
||||
411
frontend/pages/accounting/depreciation.vue
Normal file
411
frontend/pages/accounting/depreciation.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<script setup lang="ts">
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
DEPRECIATION_METHOD_ITEMS,
|
||||
ensureDepreciationDefaults,
|
||||
getAssetDepreciationStatus,
|
||||
getIncomingInvoiceDepreciationRows,
|
||||
getStatementAllocationDepreciationRow,
|
||||
isDepreciationBookingMode,
|
||||
normalizeIncomingInvoiceAccount
|
||||
} from "~/composables/useDepreciation"
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const incomingInvoices = ref<any[]>([])
|
||||
const statementAllocations = ref<any[]>([])
|
||||
const asOfDate = ref(dayjs().format("YYYY-MM-DD"))
|
||||
const selectedAsset = ref<any | null>(null)
|
||||
const editState = ref<any | null>(null)
|
||||
const editOpen = computed({
|
||||
get: () => !!selectedAsset.value,
|
||||
set: (value: boolean) => {
|
||||
if (!value) closeEdit()
|
||||
}
|
||||
})
|
||||
|
||||
const formatCurrency = (value: number) => new Intl.NumberFormat("de-DE", {
|
||||
style: "currency",
|
||||
currency: "EUR"
|
||||
}).format(Number(value || 0))
|
||||
|
||||
const isRelevantInputInvoice = (invoice: any) => invoice?.state === "Gebucht" && !!invoice?.date
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const [invoices, allocations] = await Promise.all([
|
||||
useEntities("incominginvoices").select("*, vendor(*)"),
|
||||
useEntities("statementallocations").select("*, bankstatement(*), vendor(*), customer(*)")
|
||||
])
|
||||
|
||||
incomingInvoices.value = (invoices || []).filter(isRelevantInputInvoice)
|
||||
statementAllocations.value = (allocations || []).filter((item: any) => Number(item?.amount || 0) < 0)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const depreciationAssets = computed(() => {
|
||||
const invoiceAssets = incomingInvoices.value.flatMap((invoice: any) => {
|
||||
return (invoice.accounts || [])
|
||||
.map((account: any, index: number) => {
|
||||
const normalized = ensureDepreciationDefaults(normalizeIncomingInvoiceAccount(account, invoice.date), invoice.date)
|
||||
if (!isDepreciationBookingMode(normalized.bookingMode)) return null
|
||||
|
||||
const status = getAssetDepreciationStatus({
|
||||
amountNet: Number(normalized.amountNet || 0),
|
||||
depreciationMonths: normalized.depreciationMonths,
|
||||
depreciationStartDate: normalized.depreciationStartDate || invoice.date,
|
||||
depreciationMethod: normalized.depreciationMethod,
|
||||
residualValue: normalized.residualValue,
|
||||
}, asOfDate.value)
|
||||
|
||||
const currentPeriodRow = getIncomingInvoiceDepreciationRows(invoice, dayjs(asOfDate.value).startOf("month"), dayjs(asOfDate.value).endOf("month"))
|
||||
.find((row: any) => row.index === index)
|
||||
|
||||
return {
|
||||
key: `invoice-${invoice.id}-${index}`,
|
||||
sourceType: "incominginvoice",
|
||||
sourceId: invoice.id,
|
||||
accountIndex: index,
|
||||
label: normalized.depreciationLabel || normalized.description || invoice.reference || `Eingangsbeleg ${invoice.id}`,
|
||||
group: normalized.depreciationGroup || null,
|
||||
mode: normalized.bookingMode,
|
||||
method: normalized.depreciationMethod,
|
||||
months: Number(normalized.depreciationMonths || 0),
|
||||
startDate: normalized.depreciationStartDate || invoice.date,
|
||||
residualValue: Number(normalized.residualValue || 0),
|
||||
originalValue: Number(normalized.amountNet || 0),
|
||||
currentPeriodAmount: Number(currentPeriodRow?.amount || 0),
|
||||
depreciated: status.depreciated,
|
||||
remaining: status.remaining,
|
||||
progressPercent: status.progressPercent,
|
||||
vendorName: invoice.vendor?.name || "-",
|
||||
reference: invoice.reference || "-",
|
||||
sourceRecord: invoice,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
})
|
||||
|
||||
const allocationAssets = statementAllocations.value
|
||||
.map((allocation: any) => {
|
||||
if (!isDepreciationBookingMode(allocation?.bookingMode)) return null
|
||||
|
||||
const status = getAssetDepreciationStatus({
|
||||
amount: Math.abs(Number(allocation.amount || 0)),
|
||||
depreciationMonths: allocation.depreciationMonths,
|
||||
depreciationStartDate: allocation.depreciationStartDate || allocation.created_at,
|
||||
depreciationMethod: allocation.depreciationMethod,
|
||||
residualValue: allocation.residualValue,
|
||||
}, asOfDate.value)
|
||||
|
||||
const currentPeriodRow = getStatementAllocationDepreciationRow(allocation, dayjs(asOfDate.value).startOf("month"), dayjs(asOfDate.value).endOf("month"))
|
||||
|
||||
return {
|
||||
key: `allocation-${allocation.id}`,
|
||||
sourceType: "statementallocation",
|
||||
sourceId: allocation.id,
|
||||
accountIndex: null,
|
||||
label: allocation.depreciationLabel || allocation.description || "Direkte Abschreibung",
|
||||
group: allocation.depreciationGroup || null,
|
||||
mode: allocation.bookingMode,
|
||||
method: allocation.depreciationMethod || "linear",
|
||||
months: Number(allocation.depreciationMonths || 0),
|
||||
startDate: allocation.depreciationStartDate || allocation.created_at,
|
||||
residualValue: Number(allocation.residualValue || 0),
|
||||
originalValue: Math.abs(Number(allocation.amount || 0)),
|
||||
currentPeriodAmount: Number(currentPeriodRow?.amount || 0),
|
||||
depreciated: status.depreciated,
|
||||
remaining: status.remaining,
|
||||
progressPercent: status.progressPercent,
|
||||
vendorName: allocation.vendor?.name || allocation.customer?.name || "-",
|
||||
reference: allocation.description || "-",
|
||||
sourceRecord: allocation,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
return [...invoiceAssets, ...allocationAssets]
|
||||
.sort((left: any, right: any) => Number(right.originalValue) - Number(left.originalValue))
|
||||
})
|
||||
|
||||
const groupedAssets = computed(() => {
|
||||
const groups = new Map<string, any>()
|
||||
|
||||
depreciationAssets.value.forEach((asset: any) => {
|
||||
const groupKey = asset.group || asset.key
|
||||
const current = groups.get(groupKey) || {
|
||||
key: groupKey,
|
||||
label: asset.group || asset.label,
|
||||
isBundle: !!asset.group,
|
||||
assets: [],
|
||||
originalValue: 0,
|
||||
depreciated: 0,
|
||||
remaining: 0,
|
||||
currentPeriodAmount: 0,
|
||||
}
|
||||
|
||||
current.assets.push(asset)
|
||||
current.originalValue += asset.originalValue
|
||||
current.depreciated += asset.depreciated
|
||||
current.remaining += asset.remaining
|
||||
current.currentPeriodAmount += asset.currentPeriodAmount
|
||||
groups.set(groupKey, current)
|
||||
})
|
||||
|
||||
return Array.from(groups.values()).map((group: any) => ({
|
||||
...group,
|
||||
originalValue: Number(group.originalValue.toFixed(2)),
|
||||
depreciated: Number(group.depreciated.toFixed(2)),
|
||||
remaining: Number(group.remaining.toFixed(2)),
|
||||
currentPeriodAmount: Number(group.currentPeriodAmount.toFixed(2)),
|
||||
residualValue: Number(group.assets.reduce((sum: number, asset: any) => sum + Number(asset.residualValue || 0), 0).toFixed(2)),
|
||||
progressPercent: getProgressPercent(
|
||||
group.depreciated,
|
||||
group.originalValue,
|
||||
group.assets.reduce((sum: number, asset: any) => sum + Number(asset.residualValue || 0), 0)
|
||||
),
|
||||
}))
|
||||
})
|
||||
|
||||
const totals = computed(() => {
|
||||
return groupedAssets.value.reduce((sum: any, group: any) => ({
|
||||
originalValue: Number((sum.originalValue + group.originalValue).toFixed(2)),
|
||||
depreciated: Number((sum.depreciated + group.depreciated).toFixed(2)),
|
||||
remaining: Number((sum.remaining + group.remaining).toFixed(2)),
|
||||
currentPeriodAmount: Number((sum.currentPeriodAmount + group.currentPeriodAmount).toFixed(2)),
|
||||
count: sum.count + group.assets.length,
|
||||
bundleCount: sum.bundleCount + (group.isBundle ? 1 : 0),
|
||||
}), { originalValue: 0, depreciated: 0, remaining: 0, currentPeriodAmount: 0, count: 0, bundleCount: 0 })
|
||||
})
|
||||
|
||||
const getProgressPercent = (depreciated: number, originalValue: number, residualValue: number) => {
|
||||
const depreciableBase = Math.max(0, Number(originalValue || 0) - Number(residualValue || 0))
|
||||
if (!depreciableBase) return 0
|
||||
return Math.min(100, Number(((Number(depreciated || 0) / depreciableBase) * 100).toFixed(2)))
|
||||
}
|
||||
|
||||
const startEdit = (asset: any) => {
|
||||
selectedAsset.value = asset
|
||||
editState.value = {
|
||||
depreciationLabel: asset.label,
|
||||
depreciationGroup: asset.group || "",
|
||||
depreciationMethod: asset.method || "linear",
|
||||
depreciationMonths: asset.months || 36,
|
||||
depreciationStartDate: dayjs(asset.startDate).format("YYYY-MM-DD"),
|
||||
residualValue: asset.residualValue || 0,
|
||||
}
|
||||
}
|
||||
|
||||
const closeEdit = () => {
|
||||
selectedAsset.value = null
|
||||
editState.value = null
|
||||
}
|
||||
|
||||
const saveAsset = async () => {
|
||||
if (!selectedAsset.value || !editState.value) return
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
if (selectedAsset.value.sourceType === "incominginvoice") {
|
||||
const source = selectedAsset.value.sourceRecord
|
||||
const nextAccounts = [...(source.accounts || [])]
|
||||
nextAccounts[selectedAsset.value.accountIndex] = {
|
||||
...nextAccounts[selectedAsset.value.accountIndex],
|
||||
depreciationLabel: editState.value.depreciationLabel,
|
||||
depreciationGroup: selectedAsset.value.mode === "depreciation_bundle" ? editState.value.depreciationGroup : "",
|
||||
depreciationMethod: editState.value.depreciationMethod,
|
||||
depreciationMonths: Number(editState.value.depreciationMonths || 0),
|
||||
depreciationStartDate: editState.value.depreciationStartDate,
|
||||
residualValue: Number(editState.value.residualValue || 0),
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...source,
|
||||
vendor: source.vendor?.id || source.vendor,
|
||||
accounts: nextAccounts,
|
||||
}
|
||||
delete payload.statementallocations
|
||||
delete payload.files
|
||||
await useEntities("incominginvoices").update(source.id, payload, true)
|
||||
} else {
|
||||
const source = selectedAsset.value.sourceRecord
|
||||
await useEntities("statementallocations").update(source.id, {
|
||||
depreciationLabel: editState.value.depreciationLabel,
|
||||
depreciationGroup: selectedAsset.value.mode === "depreciation_bundle" ? editState.value.depreciationGroup : null,
|
||||
depreciationMethod: editState.value.depreciationMethod,
|
||||
depreciationMonths: Number(editState.value.depreciationMonths || 0),
|
||||
depreciationStartDate: editState.value.depreciationStartDate,
|
||||
residualValue: Number(editState.value.residualValue || 0),
|
||||
}, true)
|
||||
}
|
||||
|
||||
toast.add({ title: "Abschreibung gespeichert", color: "success" })
|
||||
closeEdit()
|
||||
await loadData()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardNavbar title="Abschreibungen">
|
||||
<template #right>
|
||||
<div class="flex items-center gap-2">
|
||||
<UInput v-model="asOfDate" type="date" class="w-44" />
|
||||
<UButton icon="i-heroicons-arrow-path" variant="outline" :loading="loading" @click="loadData">
|
||||
Aktualisieren
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardPanelContent class="space-y-6 p-4 md:p-6">
|
||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Anschaffungswert</div>
|
||||
<div class="mt-2 text-2xl font-semibold">{{ formatCurrency(totals.originalValue) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ totals.count }} Abschreibungspositionen</div>
|
||||
</UCard>
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Bereits abgeschrieben</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-primary-600 dark:text-primary-400">{{ formatCurrency(totals.depreciated) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">Stand {{ dayjs(asOfDate).format("DD.MM.YYYY") }}</div>
|
||||
</UCard>
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Restbuchwert</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-amber-600 dark:text-amber-400">{{ formatCurrency(totals.remaining) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">Nach Restwertlogik</div>
|
||||
</UCard>
|
||||
<UCard>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Aktuelle Abschreibung</div>
|
||||
<div class="mt-2 text-2xl font-semibold text-error">{{ formatCurrency(totals.currentPeriodAmount) }}</div>
|
||||
<div class="mt-2 text-sm text-gray-500 dark:text-gray-400">{{ totals.bundleCount }} Sammelposten</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<UCard v-for="group in groupedAssets" :key="group.key">
|
||||
<template #header>
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">{{ group.label }}</span>
|
||||
<UBadge v-if="group.isBundle" color="warning" variant="soft">Sammelposten</UBadge>
|
||||
<UBadge color="neutral" variant="soft">{{ group.assets.length }} Positionen</UBadge>
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ formatCurrency(group.depreciated) }} abgeschrieben | {{ formatCurrency(group.remaining) }} Restbuchwert
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-52 space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>Fortschritt</span>
|
||||
<span>{{ group.progressPercent.toFixed(1) }}%</span>
|
||||
</div>
|
||||
<div class="h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
|
||||
<div
|
||||
class="h-full rounded-full bg-primary-500"
|
||||
:style="{ width: `${group.progressPercent}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div v-for="asset in group.assets" :key="asset.key" class="rounded-lg border border-gray-200 dark:border-gray-800 p-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div class="space-y-1">
|
||||
<div class="font-medium text-gray-900 dark:text-white">{{ asset.label }}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ asset.vendorName }} | {{ asset.reference }}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<UBadge color="neutral" variant="soft">{{ asset.method === "degressive" ? "Degressiv" : "Linear" }}</UBadge>
|
||||
<UBadge color="neutral" variant="soft">{{ asset.months }} Monate</UBadge>
|
||||
<UBadge color="neutral" variant="soft">Start {{ dayjs(asset.startDate).format("MM/YYYY") }}</UBadge>
|
||||
<UBadge color="neutral" variant="soft">Restwert {{ formatCurrency(asset.residualValue) }}</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
<UButton size="sm" variant="outline" icon="i-heroicons-pencil-square" @click="startEdit(asset)">
|
||||
Bearbeiten
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-4">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Anschaffungswert</div>
|
||||
<div class="mt-1 font-semibold">{{ formatCurrency(asset.originalValue) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Abgeschrieben</div>
|
||||
<div class="mt-1 font-semibold text-primary-600 dark:text-primary-400">{{ formatCurrency(asset.depreciated) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Aktueller Zeitraum</div>
|
||||
<div class="mt-1 font-semibold text-error">{{ formatCurrency(asset.currentPeriodAmount) }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Restbuchwert</div>
|
||||
<div class="mt-1 font-semibold text-amber-600 dark:text-amber-400">{{ formatCurrency(asset.remaining) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span>Wirklicher Abschreibungsfortschritt</span>
|
||||
<span>{{ getProgressPercent(asset.depreciated, asset.originalValue, asset.residualValue).toFixed(1) }}%</span>
|
||||
</div>
|
||||
<div class="h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
|
||||
<div
|
||||
class="h-full rounded-full bg-primary-500"
|
||||
:style="{ width: `${getProgressPercent(asset.depreciated, asset.originalValue, asset.residualValue)}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
<USlideover v-model:open="editOpen" title="Abschreibung bearbeiten">
|
||||
<template #body>
|
||||
<div v-if="selectedAsset && editState" class="space-y-4 p-4">
|
||||
<UFormField label="Bezeichnung">
|
||||
<UInput v-model="editState.depreciationLabel" />
|
||||
</UFormField>
|
||||
<UFormField v-if="selectedAsset.mode === 'depreciation_bundle'" label="Sammelposten">
|
||||
<UInput v-model="editState.depreciationGroup" />
|
||||
</UFormField>
|
||||
<UFormField label="Methode">
|
||||
<USelectMenu v-model="editState.depreciationMethod" :items="DEPRECIATION_METHOD_ITEMS" value-key="value" label-key="label" />
|
||||
</UFormField>
|
||||
<UFormField label="Dauer (Monate)">
|
||||
<UInput v-model="editState.depreciationMonths" type="number" min="1" step="1" />
|
||||
</UFormField>
|
||||
<UFormField label="Start Abschreibung">
|
||||
<UInput v-model="editState.depreciationStartDate" type="date" />
|
||||
</UFormField>
|
||||
<UFormField label="Restwert">
|
||||
<UInput v-model="editState.residualValue" type="number" min="0" step="0.01" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2 p-4">
|
||||
<UButton variant="ghost" @click="closeEdit">Abbrechen</UButton>
|
||||
<UButton :loading="saving" @click="saveAsset">Speichern</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</USlideover>
|
||||
</UDashboardPanelContent>
|
||||
</template>
|
||||
Reference in New Issue
Block a user