Added Abschreibungen
This commit is contained in:
@@ -136,6 +136,11 @@ const links = computed(() => {
|
||||
to: "/incomingInvoices",
|
||||
icon: "i-heroicons-document-text",
|
||||
} : null,
|
||||
(featureEnabled("incomingInvoices") || featureEnabled("banking")) ? {
|
||||
label: "Abschreibungen",
|
||||
to: "/accounting/depreciation",
|
||||
icon: "i-heroicons-calendar-days",
|
||||
} : null,
|
||||
((featureEnabled("createDocument") || featureEnabled("incomingInvoices")) || featureEnabled("accounts") || featureEnabled("ownaccounts") || featureEnabled("costcentres")) ? {
|
||||
label: "Auswertungen",
|
||||
icon: "i-heroicons-chart-pie",
|
||||
|
||||
@@ -4,16 +4,24 @@ import {
|
||||
getCreatedDocumentTaxBreakdown,
|
||||
getIncomingInvoiceTaxBreakdown
|
||||
} from "~/composables/useTaxEvaluation"
|
||||
import {
|
||||
getIncomingInvoiceDepreciationRows,
|
||||
getIncomingInvoiceImmediateExpenseNet,
|
||||
getStatementAllocationDepreciationRow,
|
||||
getStatementAllocationImmediateExpenseAmount
|
||||
} from "~/composables/useDepreciation"
|
||||
|
||||
const loading = ref(true)
|
||||
const summary = ref({
|
||||
label: "",
|
||||
income: 0,
|
||||
expenses: 0,
|
||||
depreciations: 0,
|
||||
result: 0,
|
||||
taxBalance: 0,
|
||||
incomeCount: 0,
|
||||
expenseCount: 0
|
||||
expenseCount: 0,
|
||||
depreciationCount: 0
|
||||
})
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
@@ -91,13 +99,20 @@ const loadSummary = async () => {
|
||||
}, 0)
|
||||
|
||||
const invoiceExpenses = inputDocs.reduce((sum: number, invoice: any) => {
|
||||
return sum + (invoice.accounts || []).reduce((accountSum: number, account: any) => accountSum + Number(account.amountNet || 0), 0)
|
||||
return sum + getIncomingInvoiceImmediateExpenseNet(invoice)
|
||||
}, 0)
|
||||
|
||||
const directAccountExpenses = directExpenses.reduce((sum: number, allocation: any) => {
|
||||
return sum + Math.abs(Number(allocation.amount || 0))
|
||||
return sum + getStatementAllocationImmediateExpenseAmount(allocation)
|
||||
}, 0)
|
||||
|
||||
const depreciationRows = [
|
||||
...inputDocs.flatMap((invoice: any) => getIncomingInvoiceDepreciationRows(invoice, bounds.start, bounds.end)),
|
||||
...(allocations || []).map((allocation: any) => getStatementAllocationDepreciationRow(allocation, bounds.start, bounds.end)).filter(Boolean)
|
||||
]
|
||||
|
||||
const depreciations = depreciationRows.reduce((sum: number, row: any) => sum + Number(row.amount || 0), 0)
|
||||
|
||||
const outputTax = outputDocs.reduce((sum: number, doc: any) => {
|
||||
const breakdown = getCreatedDocumentTaxBreakdown(doc)
|
||||
return sum + breakdown.tax19 + breakdown.tax7
|
||||
@@ -108,16 +123,18 @@ const loadSummary = async () => {
|
||||
return sum + breakdown.tax19 + breakdown.tax7
|
||||
}, 0)
|
||||
|
||||
const expenses = invoiceExpenses + directAccountExpenses
|
||||
const expenses = invoiceExpenses + directAccountExpenses + depreciations
|
||||
|
||||
summary.value = {
|
||||
label: dayjs().format("MMMM YYYY"),
|
||||
income: Number(income.toFixed(2)),
|
||||
expenses: Number(expenses.toFixed(2)),
|
||||
depreciations: Number(depreciations.toFixed(2)),
|
||||
result: Number((income - expenses).toFixed(2)),
|
||||
taxBalance: Number((outputTax - inputTax).toFixed(2)),
|
||||
incomeCount: outputDocs.length,
|
||||
expenseCount: inputDocs.length + directExpenses.length
|
||||
expenseCount: inputDocs.filter((invoice: any) => getIncomingInvoiceImmediateExpenseNet(invoice) > 0).length + directExpenses.filter((allocation: any) => getStatementAllocationImmediateExpenseAmount(allocation) > 0).length,
|
||||
depreciationCount: depreciationRows.length
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -159,6 +176,13 @@ onMounted(loadSummary)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bwa-summary-row">
|
||||
<span class="bwa-summary-label">Davon Abschreibungen</span>
|
||||
<span class="bwa-summary-value text-amber-600">
|
||||
{{ loading ? "..." : formatCurrency(summary.depreciations) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="bwa-summary-row">
|
||||
<span class="bwa-summary-label">Ergebnis</span>
|
||||
<span class="bwa-summary-value" :class="summary.result >= 0 ? 'text-primary-500' : 'text-error'">
|
||||
@@ -167,7 +191,7 @@ onMounted(loadSummary)
|
||||
</div>
|
||||
|
||||
<div class="bwa-summary-meta">
|
||||
{{ summary.incomeCount }} Einnahmenbelege | {{ summary.expenseCount }} Ausgabenbelege | USt-Saldo {{ formatCurrency(summary.taxBalance) }}
|
||||
{{ summary.incomeCount }} Einnahmenbelege | {{ summary.expenseCount }} Ausgabenbelege | {{ summary.depreciationCount }} Abschreibungen | USt-Saldo {{ formatCurrency(summary.taxBalance) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
394
frontend/composables/useDepreciation.ts
Normal file
394
frontend/composables/useDepreciation.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import dayjs from "dayjs"
|
||||
|
||||
export const EXPENSE_BOOKING_MODE_ITEMS = [
|
||||
{ label: "Sofortaufwand", value: "expense" },
|
||||
{ label: "Abschreibung einzeln", value: "depreciation_single" },
|
||||
{ label: "Abschreibung Sammelposten", value: "depreciation_bundle" },
|
||||
]
|
||||
|
||||
export const DEPRECIATION_METHOD_ITEMS = [
|
||||
{ label: "Linear", value: "linear" },
|
||||
{ label: "Degressiv", value: "degressive" },
|
||||
]
|
||||
|
||||
export const normalizeExpenseBookingMode = (value?: string | null) => {
|
||||
if (value === "depreciation_single" || value === "depreciation_bundle") return value
|
||||
return "expense"
|
||||
}
|
||||
|
||||
export const isDepreciationBookingMode = (value?: string | null) => {
|
||||
return normalizeExpenseBookingMode(value) !== "expense"
|
||||
}
|
||||
|
||||
export const normalizeDepreciationMethod = (value?: string | null) => {
|
||||
return value === "degressive" ? "degressive" : "linear"
|
||||
}
|
||||
|
||||
export const createIncomingInvoiceAccount = (overrides: Record<string, any> = {}) => ({
|
||||
account: null,
|
||||
amountNet: null,
|
||||
amountTax: null,
|
||||
taxType: "19",
|
||||
costCentre: null,
|
||||
amountGross: null,
|
||||
description: "",
|
||||
bookingMode: "expense",
|
||||
depreciationMonths: null,
|
||||
depreciationStartDate: null,
|
||||
depreciationMethod: "linear",
|
||||
depreciationLabel: "",
|
||||
depreciationGroup: "",
|
||||
residualValue: 0,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
export const normalizeIncomingInvoiceAccount = (account: Record<string, any> = {}, fallbackDate?: any) => {
|
||||
const bookingMode = normalizeExpenseBookingMode(account?.bookingMode)
|
||||
|
||||
return {
|
||||
...createIncomingInvoiceAccount(account),
|
||||
bookingMode,
|
||||
depreciationMonths: account?.depreciationMonths ? Number(account.depreciationMonths) : null,
|
||||
depreciationStartDate: account?.depreciationStartDate || fallbackDate || null,
|
||||
depreciationMethod: normalizeDepreciationMethod(account?.depreciationMethod),
|
||||
depreciationLabel: String(account?.depreciationLabel || account?.description || "").trim(),
|
||||
depreciationGroup: String(account?.depreciationGroup || "").trim(),
|
||||
residualValue: Number(account?.residualValue || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeIncomingInvoiceAccounts = (accounts: any[] = [], fallbackDate?: any) => {
|
||||
const normalized = (accounts || []).map((account) => normalizeIncomingInvoiceAccount(account, fallbackDate))
|
||||
return normalized.length > 0 ? normalized : [createIncomingInvoiceAccount({ depreciationStartDate: fallbackDate || null })]
|
||||
}
|
||||
|
||||
export const ensureDepreciationDefaults = (item: Record<string, any>, fallbackDate?: any) => {
|
||||
item.bookingMode = normalizeExpenseBookingMode(item?.bookingMode)
|
||||
|
||||
if (!isDepreciationBookingMode(item.bookingMode)) {
|
||||
item.depreciationMonths = null
|
||||
item.depreciationStartDate = null
|
||||
item.depreciationMethod = "linear"
|
||||
item.depreciationGroup = ""
|
||||
item.residualValue = 0
|
||||
if (!item.depreciationLabel) item.depreciationLabel = item.description || ""
|
||||
return item
|
||||
}
|
||||
|
||||
if (!Number(item.depreciationMonths)) item.depreciationMonths = 36
|
||||
if (!item.depreciationStartDate) item.depreciationStartDate = fallbackDate || null
|
||||
item.depreciationMethod = normalizeDepreciationMethod(item.depreciationMethod)
|
||||
if (!item.depreciationLabel) item.depreciationLabel = item.description || ""
|
||||
if (item.bookingMode !== "depreciation_bundle") item.depreciationGroup = ""
|
||||
item.residualValue = Number(item.residualValue || 0)
|
||||
return item
|
||||
}
|
||||
|
||||
const distributeLinearDepreciation = (amount: number, months: number) => {
|
||||
const totalCents = Math.round(Number(amount || 0) * 100)
|
||||
const totalMonths = Math.max(1, Math.round(Number(months || 0)))
|
||||
const base = Math.trunc(totalCents / totalMonths)
|
||||
const remainder = totalCents - (base * totalMonths)
|
||||
|
||||
return Array.from({ length: totalMonths }, (_, index) => {
|
||||
return base + (index < remainder ? 1 : 0)
|
||||
})
|
||||
}
|
||||
|
||||
const distributeDegressiveDepreciation = (amount: number, months: number, residualValue: number) => {
|
||||
const startCents = Math.round(Math.max(0, Number(amount || 0)) * 100)
|
||||
const residualCents = Math.round(Math.max(0, Number(residualValue || 0)) * 100)
|
||||
const totalMonths = Math.max(1, Math.round(Number(months || 0)))
|
||||
const targetResidual = Math.min(startCents, residualCents)
|
||||
const depreciable = Math.max(0, startCents - targetResidual)
|
||||
|
||||
if (!depreciable) return Array.from({ length: totalMonths }, () => 0)
|
||||
|
||||
// Monthly degressive rate derived from start and target residual over the selected duration.
|
||||
const rate = targetResidual > 0
|
||||
? 1 - Math.pow(targetResidual / startCents, 1 / totalMonths)
|
||||
: 1 - Math.pow(0.01 / Math.max(startCents, 1), 1 / totalMonths)
|
||||
|
||||
const values: number[] = []
|
||||
let currentBookValue = startCents
|
||||
let depreciated = 0
|
||||
|
||||
for (let index = 0; index < totalMonths; index += 1) {
|
||||
const remainingDepreciable = Math.max(0, depreciable - depreciated)
|
||||
if (remainingDepreciable <= 0) {
|
||||
values.push(0)
|
||||
continue
|
||||
}
|
||||
|
||||
if (index === totalMonths - 1) {
|
||||
values.push(remainingDepreciable)
|
||||
depreciated += remainingDepreciable
|
||||
currentBookValue = targetResidual
|
||||
continue
|
||||
}
|
||||
|
||||
const raw = Math.round(currentBookValue * rate)
|
||||
const maxAllowed = Math.max(1, remainingDepreciable - Math.max(0, totalMonths - index - 1))
|
||||
const monthValue = Math.max(1, Math.min(raw || 1, maxAllowed))
|
||||
values.push(monthValue)
|
||||
depreciated += monthValue
|
||||
currentBookValue = Math.max(targetResidual, currentBookValue - monthValue)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
export const getDepreciationPlan = ({
|
||||
amount,
|
||||
months,
|
||||
startDate,
|
||||
method,
|
||||
residualValue,
|
||||
}: {
|
||||
amount: number
|
||||
months: number
|
||||
startDate: any
|
||||
method?: string
|
||||
residualValue?: number
|
||||
}) => {
|
||||
const start = dayjs(startDate).startOf("month")
|
||||
const totalAmount = Number(amount || 0)
|
||||
const totalMonths = Math.max(1, Math.round(Number(months || 0)))
|
||||
const normalizedMethod = normalizeDepreciationMethod(method)
|
||||
const normalizedResidual = Number(residualValue || 0)
|
||||
|
||||
if (!start.isValid() || totalAmount <= 0) return []
|
||||
|
||||
const distributed = normalizedMethod === "degressive"
|
||||
? distributeDegressiveDepreciation(totalAmount, totalMonths, normalizedResidual)
|
||||
: distributeLinearDepreciation(Math.max(0, totalAmount - normalizedResidual), totalMonths)
|
||||
|
||||
return distributed.map((cents, index) => ({
|
||||
index,
|
||||
date: start.add(index, "month"),
|
||||
amount: Number((cents / 100).toFixed(2)),
|
||||
}))
|
||||
}
|
||||
|
||||
export const getLinearDepreciationAmountForRange = ({
|
||||
amount,
|
||||
months,
|
||||
startDate,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
method,
|
||||
residualValue,
|
||||
}: {
|
||||
amount: number
|
||||
months: number
|
||||
startDate: any
|
||||
rangeStart: any
|
||||
rangeEnd: any
|
||||
method?: string
|
||||
residualValue?: number
|
||||
}) => {
|
||||
const start = dayjs(startDate).startOf("month")
|
||||
const periodStart = dayjs(rangeStart).startOf("month")
|
||||
const periodEnd = dayjs(rangeEnd).endOf("month")
|
||||
|
||||
if (!start.isValid() || !periodStart.isValid() || !periodEnd.isValid()) return 0
|
||||
|
||||
const distributed = getDepreciationPlan({
|
||||
amount,
|
||||
months,
|
||||
startDate,
|
||||
method,
|
||||
residualValue,
|
||||
})
|
||||
let resultCents = 0
|
||||
|
||||
distributed.forEach((value) => {
|
||||
const monthDate = dayjs(value.date)
|
||||
if ((monthDate.isAfter(periodStart) || monthDate.isSame(periodStart, "month"))
|
||||
&& (monthDate.isBefore(periodEnd) || monthDate.isSame(periodEnd, "month"))) {
|
||||
resultCents += Math.round(Number(value.amount || 0) * 100)
|
||||
}
|
||||
})
|
||||
|
||||
return Number((resultCents / 100).toFixed(2))
|
||||
}
|
||||
|
||||
export const getIncomingInvoiceImmediateExpenseNet = (invoice: any) => {
|
||||
return Number(((invoice?.accounts || []).reduce((sum: number, account: any) => {
|
||||
const normalized = normalizeIncomingInvoiceAccount(account, invoice?.date)
|
||||
if (isDepreciationBookingMode(normalized.bookingMode)) return sum
|
||||
return sum + Number(normalized.amountNet || 0)
|
||||
}, 0)).toFixed(2))
|
||||
}
|
||||
|
||||
export const getIncomingInvoiceImmediateExpenseGross = (invoice: any) => {
|
||||
return Number(((invoice?.accounts || []).reduce((sum: number, account: any) => {
|
||||
const normalized = normalizeIncomingInvoiceAccount(account, invoice?.date)
|
||||
if (isDepreciationBookingMode(normalized.bookingMode)) return sum
|
||||
|
||||
const amountGross = Number(normalized.amountGross)
|
||||
return sum + (Number.isFinite(amountGross) ? amountGross : Number(normalized.amountNet || 0) + Number(normalized.amountTax || 0))
|
||||
}, 0)).toFixed(2))
|
||||
}
|
||||
|
||||
export const getIncomingInvoiceDepreciationRows = (invoice: any, rangeStart: any, rangeEnd: 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 amount = Number(normalized.amountNet || 0)
|
||||
const months = Number(normalized.depreciationMonths || 0)
|
||||
const plan = getDepreciationPlan({
|
||||
amount,
|
||||
months,
|
||||
startDate: normalized.depreciationStartDate || invoice?.date,
|
||||
method: normalized.depreciationMethod,
|
||||
residualValue: normalized.residualValue,
|
||||
})
|
||||
const periodAmount = Number(plan.reduce((sum, item) => {
|
||||
const monthDate = dayjs(item.date)
|
||||
const periodStart = dayjs(rangeStart).startOf("month")
|
||||
const periodEnd = dayjs(rangeEnd).endOf("month")
|
||||
if ((monthDate.isAfter(periodStart) || monthDate.isSame(periodStart, "month"))
|
||||
&& (monthDate.isBefore(periodEnd) || monthDate.isSame(periodEnd, "month"))) {
|
||||
return sum + Number(item.amount || 0)
|
||||
}
|
||||
return sum
|
||||
}, 0).toFixed(2))
|
||||
|
||||
if (!periodAmount) return null
|
||||
|
||||
const alreadyDepreciated = Number(plan
|
||||
.filter((entry) => dayjs(entry.date).isBefore(dayjs().startOf("month")) || dayjs(entry.date).isSame(dayjs().startOf("month"), "month"))
|
||||
.reduce((sum, entry) => sum + Number(entry.amount || 0), 0)
|
||||
.toFixed(2))
|
||||
const residualBookValue = Number(Math.max(0, amount - alreadyDepreciated).toFixed(2))
|
||||
|
||||
return {
|
||||
source: "incominginvoice",
|
||||
sourceId: invoice?.id,
|
||||
index,
|
||||
mode: normalized.bookingMode,
|
||||
label: normalized.depreciationLabel || normalized.description || invoice?.reference || `Eingangsbeleg ${invoice?.id}`,
|
||||
group: normalized.depreciationGroup || null,
|
||||
amount: periodAmount,
|
||||
months,
|
||||
startDate: normalized.depreciationStartDate || invoice?.date,
|
||||
method: normalized.depreciationMethod,
|
||||
residualValue: Number(normalized.residualValue || 0),
|
||||
alreadyDepreciated,
|
||||
residualBookValue,
|
||||
vendor: invoice?.vendor,
|
||||
reference: invoice?.reference,
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export const getStatementAllocationImmediateExpenseAmount = (allocation: any) => {
|
||||
const mode = normalizeExpenseBookingMode(allocation?.bookingMode)
|
||||
const amount = Number(allocation?.amount || 0)
|
||||
if (mode !== "expense" || amount >= 0) return 0
|
||||
return Number(Math.abs(amount).toFixed(2))
|
||||
}
|
||||
|
||||
export const getStatementAllocationDepreciationAmount = (allocation: any, rangeStart: any, rangeEnd: any) => {
|
||||
const mode = normalizeExpenseBookingMode(allocation?.bookingMode)
|
||||
const rawAmount = Number(allocation?.amount || 0)
|
||||
const amount = Math.abs(rawAmount)
|
||||
if (mode === "expense" || rawAmount >= 0 || amount <= 0) return 0
|
||||
|
||||
return getLinearDepreciationAmountForRange({
|
||||
amount,
|
||||
months: Number(allocation?.depreciationMonths || 0),
|
||||
startDate: allocation?.depreciationStartDate || allocation?.created_at,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
method: allocation?.depreciationMethod,
|
||||
residualValue: allocation?.residualValue,
|
||||
})
|
||||
}
|
||||
|
||||
export const getStatementAllocationDepreciationRow = (allocation: any, rangeStart: any, rangeEnd: any) => {
|
||||
const totalAmount = Math.abs(Number(allocation?.amount || 0))
|
||||
const months = Number(allocation?.depreciationMonths || 0)
|
||||
const startDate = allocation?.depreciationStartDate || allocation?.created_at
|
||||
const method = normalizeDepreciationMethod(allocation?.depreciationMethod)
|
||||
const residualValue = Number(allocation?.residualValue || 0)
|
||||
const plan = getDepreciationPlan({
|
||||
amount: totalAmount,
|
||||
months,
|
||||
startDate,
|
||||
method,
|
||||
residualValue,
|
||||
})
|
||||
const amount = Number(plan.reduce((sum, item) => {
|
||||
const monthDate = dayjs(item.date)
|
||||
const periodStart = dayjs(rangeStart).startOf("month")
|
||||
const periodEnd = dayjs(rangeEnd).endOf("month")
|
||||
if ((monthDate.isAfter(periodStart) || monthDate.isSame(periodStart, "month"))
|
||||
&& (monthDate.isBefore(periodEnd) || monthDate.isSame(periodEnd, "month"))) {
|
||||
return sum + Number(item.amount || 0)
|
||||
}
|
||||
return sum
|
||||
}, 0).toFixed(2))
|
||||
if (!amount) return null
|
||||
|
||||
const alreadyDepreciated = Number(plan
|
||||
.filter((entry) => dayjs(entry.date).isBefore(dayjs().startOf("month")) || dayjs(entry.date).isSame(dayjs().startOf("month"), "month"))
|
||||
.reduce((sum, entry) => sum + Number(entry.amount || 0), 0)
|
||||
.toFixed(2))
|
||||
const residualBookValue = Number(Math.max(0, totalAmount - alreadyDepreciated).toFixed(2))
|
||||
|
||||
return {
|
||||
source: "statementallocation",
|
||||
sourceId: allocation?.id,
|
||||
mode: normalizeExpenseBookingMode(allocation?.bookingMode),
|
||||
label: allocation?.depreciationLabel || allocation?.description || "Direkte Abschreibung",
|
||||
group: allocation?.depreciationGroup || null,
|
||||
amount,
|
||||
months,
|
||||
startDate,
|
||||
method,
|
||||
residualValue,
|
||||
alreadyDepreciated,
|
||||
residualBookValue,
|
||||
}
|
||||
}
|
||||
|
||||
export const getAssetDepreciationStatus = (asset: any, asOfDate?: any) => {
|
||||
const amount = Number(asset?.amount || asset?.amountNet || 0)
|
||||
const months = Number(asset?.depreciationMonths || 0)
|
||||
const startDate = asset?.depreciationStartDate || asset?.date || asset?.created_at
|
||||
const method = normalizeDepreciationMethod(asset?.depreciationMethod)
|
||||
const residualValue = Number(asset?.residualValue || 0)
|
||||
const plan = getDepreciationPlan({
|
||||
amount,
|
||||
months,
|
||||
startDate,
|
||||
method,
|
||||
residualValue,
|
||||
})
|
||||
const cutoff = dayjs(asOfDate || new Date()).endOf("month")
|
||||
const depreciated = Number(plan
|
||||
.filter((entry) => dayjs(entry.date).isBefore(cutoff) || dayjs(entry.date).isSame(cutoff, "month"))
|
||||
.reduce((sum, entry) => sum + Number(entry.amount || 0), 0)
|
||||
.toFixed(2))
|
||||
const remaining = Number(Math.max(0, amount - depreciated).toFixed(2))
|
||||
const depreciableBase = Number(Math.max(0, amount - residualValue).toFixed(2))
|
||||
|
||||
return {
|
||||
plan,
|
||||
amount: Number(amount.toFixed(2)),
|
||||
depreciated,
|
||||
remaining,
|
||||
residualValue: Number(residualValue.toFixed(2)),
|
||||
depreciableBase,
|
||||
progressPercent: depreciableBase > 0 ? Math.min(100, Number(((depreciated / depreciableBase) * 100).toFixed(2))) : 0,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,5 +1,11 @@
|
||||
<script setup>
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
DEPRECIATION_METHOD_ITEMS,
|
||||
EXPENSE_BOOKING_MODE_ITEMS,
|
||||
isDepreciationBookingMode,
|
||||
normalizeExpenseBookingMode
|
||||
} from "~/composables/useDepreciation"
|
||||
// import {filter} from "vuedraggable/dist/vuedraggable.common.js"; // Scheint nicht genutzt zu werden, auskommentiert
|
||||
|
||||
defineShortcuts({
|
||||
@@ -43,6 +49,7 @@ const setup = async () => {
|
||||
if (itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
|
||||
|
||||
manualAllocationSum.value = calculateOpenSum.value
|
||||
allocationDepreciationStartDate.value = dayjs(itemInfo.value?.date || new Date()).format("YYYY-MM-DD")
|
||||
|
||||
createddocuments.value = (await useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"))
|
||||
const documents = createddocuments.value.filter(i => i.type === "invoices" || i.type === "advanceInvoices")
|
||||
@@ -129,13 +136,56 @@ const selectAccount = (id) => {
|
||||
|
||||
const manualAllocationSum = ref(itemInfo.value.amount || 0)
|
||||
const allocationDescription = ref("")
|
||||
const allocationBookingMode = ref("expense")
|
||||
const allocationDepreciationMonths = ref(36)
|
||||
const allocationDepreciationStartDate = ref(dayjs().format("YYYY-MM-DD"))
|
||||
const allocationDepreciationMethod = ref("linear")
|
||||
const allocationDepreciationLabel = ref("")
|
||||
const allocationDepreciationGroup = ref("")
|
||||
const allocationResidualValue = ref(0)
|
||||
const showMoreWithoutRecipe = ref(false)
|
||||
const showMoreText = ref(false)
|
||||
|
||||
const isDepreciationAllocation = computed(() => isDepreciationBookingMode(allocationBookingMode.value))
|
||||
|
||||
const buildAllocationPayload = (payload) => {
|
||||
const base = {
|
||||
...payload,
|
||||
bookingMode: normalizeExpenseBookingMode(allocationBookingMode.value)
|
||||
}
|
||||
|
||||
if (!isDepreciationAllocation.value) {
|
||||
return {
|
||||
...base,
|
||||
depreciationMonths: null,
|
||||
depreciationStartDate: null,
|
||||
depreciationLabel: null,
|
||||
depreciationGroup: null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
depreciationMonths: Number(allocationDepreciationMonths.value || 0),
|
||||
depreciationStartDate: allocationDepreciationStartDate.value || itemInfo.value?.date || null,
|
||||
depreciationMethod: allocationDepreciationMethod.value,
|
||||
depreciationLabel: allocationDepreciationLabel.value || allocationDescription.value || "Direkte Abschreibung",
|
||||
depreciationGroup: allocationBookingMode.value === "depreciation_bundle" ? allocationDepreciationGroup.value || null : null,
|
||||
residualValue: Number(allocationResidualValue.value || 0)
|
||||
}
|
||||
}
|
||||
|
||||
watch(allocationBookingMode, (value) => {
|
||||
if (!isDepreciationBookingMode(value)) return
|
||||
if (!allocationDepreciationStartDate.value) {
|
||||
allocationDepreciationStartDate.value = dayjs(itemInfo.value?.date || new Date()).format("YYYY-MM-DD")
|
||||
}
|
||||
})
|
||||
|
||||
const saveAllocation = async (allocation) => {
|
||||
const res = await useNuxtApp().$api("/api/banking/statements", {
|
||||
method: "POST",
|
||||
body: {data: allocation}
|
||||
body: {data: buildAllocationPayload(allocation)}
|
||||
})
|
||||
|
||||
if (res) {
|
||||
@@ -144,6 +194,13 @@ const saveAllocation = async (allocation) => {
|
||||
vendorAccountToSave.value = null
|
||||
customerAccountToSave.value = null
|
||||
ownAccountToSave.value = null
|
||||
allocationBookingMode.value = "expense"
|
||||
allocationDepreciationMonths.value = 36
|
||||
allocationDepreciationStartDate.value = dayjs(itemInfo.value?.date || new Date()).format("YYYY-MM-DD")
|
||||
allocationDepreciationMethod.value = "linear"
|
||||
allocationDepreciationLabel.value = ""
|
||||
allocationDepreciationGroup.value = ""
|
||||
allocationResidualValue.value = 0
|
||||
// allocationDescription.value = null // Optional: Beschreibung behalten für nächste Buchung?
|
||||
}
|
||||
}
|
||||
@@ -420,6 +477,13 @@ const getAllocationIcon = (item) => {
|
||||
return 'i-heroicons-question-mark-circle'
|
||||
}
|
||||
|
||||
const getAllocationBookingModeLabel = (item) => {
|
||||
const mode = normalizeExpenseBookingMode(item?.bookingMode)
|
||||
if (mode === "depreciation_bundle") return "Sammelabschreibung"
|
||||
if (mode === "depreciation_single") return "Einzelabschreibung"
|
||||
return "Sofortaufwand"
|
||||
}
|
||||
|
||||
setup()
|
||||
</script>
|
||||
|
||||
@@ -526,6 +590,12 @@ setup()
|
||||
{{ getAllocationLabel(item) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-0.5" v-if="item.description">{{ item.description }}</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>
|
||||
<UBadge size="xs" color="neutral" variant="soft" v-if="item.depreciationMonths">{{ item.depreciationMonths }} Monate</UBadge>
|
||||
<UBadge size="xs" color="neutral" variant="soft" v-if="item.depreciationMethod">{{ item.depreciationMethod === 'degressive' ? 'Degressiv' : 'Linear' }}</UBadge>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-1" v-if="item.createddocument || item.incominginvoice">
|
||||
<UButton
|
||||
v-if="item.createddocument"
|
||||
@@ -618,6 +688,56 @@ setup()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<UFormField label="Aufwandsart" size="sm">
|
||||
<USelectMenu
|
||||
v-model="allocationBookingMode"
|
||||
:items="EXPENSE_BOOKING_MODE_ITEMS"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Abschreibungsdauer (Monate)" size="sm" v-if="isDepreciationAllocation">
|
||||
<UInput v-model="allocationDepreciationMonths" type="number" min="1" step="1" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Methode" size="sm" v-if="isDepreciationAllocation">
|
||||
<USelectMenu
|
||||
v-model="allocationDepreciationMethod"
|
||||
:items="DEPRECIATION_METHOD_ITEMS"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3" v-if="isDepreciationAllocation">
|
||||
<UFormField label="Start Abschreibung" size="sm">
|
||||
<UInput v-model="allocationDepreciationStartDate" type="date" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField label="Restwert" size="sm">
|
||||
<UInput v-model="allocationResidualValue" type="number" min="0" step="0.01" />
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3" v-if="isDepreciationAllocation">
|
||||
<UFormField v-if="allocationBookingMode === 'depreciation_bundle'" label="Sammelposten" size="sm">
|
||||
<UInput v-model="allocationDepreciationGroup" placeholder="z. B. Betriebsausstattung 2026" />
|
||||
</UFormField>
|
||||
<UFormField v-else label="Bezeichnung Abschreibung" size="sm">
|
||||
<UInput v-model="allocationDepreciationLabel" placeholder="z. B. Werkzeugkoffer" />
|
||||
</UFormField>
|
||||
<UAlert
|
||||
color="warning"
|
||||
variant="soft"
|
||||
icon="i-heroicons-calendar-days"
|
||||
title="BWA bucht nur die Abschreibungsrate"
|
||||
description="Die Auszahlung erscheint dann nicht sofort als Aufwand, sondern periodisiert über die gewählte Laufzeit."
|
||||
/>
|
||||
</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 grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<USelectMenu :items="ownaccounts" value-key="id" label-key="name" v-model="ownAccountToSave"
|
||||
|
||||
@@ -3,6 +3,14 @@ import InputGroup from "~/components/InputGroup.vue";
|
||||
import dayjs from "dayjs";
|
||||
import { parseDate } from "@internationalized/date"
|
||||
import { useDraggable } from '@vueuse/core'
|
||||
import {
|
||||
DEPRECIATION_METHOD_ITEMS,
|
||||
EXPENSE_BOOKING_MODE_ITEMS,
|
||||
createIncomingInvoiceAccount,
|
||||
ensureDepreciationDefaults,
|
||||
isDepreciationBookingMode,
|
||||
normalizeIncomingInvoiceAccounts
|
||||
} from "~/composables/useDepreciation"
|
||||
|
||||
// --- Standard Setup & Data ---
|
||||
const dataStore = useDataStore()
|
||||
@@ -30,14 +38,7 @@ const itemInfo = ref({
|
||||
description: "",
|
||||
state: "Entwurf",
|
||||
accounts: [
|
||||
{
|
||||
account: null,
|
||||
amountNet: null,
|
||||
amountTax: null,
|
||||
taxType: "19",
|
||||
costCentre: null,
|
||||
amountGross: null
|
||||
}
|
||||
createIncomingInvoiceAccount()
|
||||
]
|
||||
})
|
||||
|
||||
@@ -61,12 +62,12 @@ const setup = async () => {
|
||||
itemInfo.value = {
|
||||
...invoiceData,
|
||||
vendor: invoiceData.vendor?.id || invoiceData.vendor,
|
||||
accounts: invoiceData.accounts || []
|
||||
accounts: normalizeIncomingInvoiceAccounts(invoiceData.accounts || [], invoiceData.date)
|
||||
}
|
||||
|
||||
// Fallback Accounts
|
||||
if(itemInfo.value.accounts.length === 0) {
|
||||
itemInfo.value.accounts.push({ account: null, amountNet: null, amountTax: null, taxType: "19", costCentre: null })
|
||||
itemInfo.value.accounts.push(createIncomingInvoiceAccount({ depreciationStartDate: itemInfo.value.date || null }))
|
||||
}
|
||||
|
||||
// Datei laden
|
||||
@@ -92,6 +93,14 @@ const setup = async () => {
|
||||
|
||||
setup()
|
||||
|
||||
watch(() => itemInfo.value.date, (value) => {
|
||||
;(itemInfo.value.accounts || []).forEach((account) => {
|
||||
if (isDepreciationItem(account) && !account.depreciationStartDate) {
|
||||
account.depreciationStartDate = value || null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// --- Berechnungslogik ---
|
||||
const useNetMode = ref(false)
|
||||
|
||||
@@ -144,6 +153,11 @@ const totalCalculated = computed(() => {
|
||||
|
||||
const hasAmount = (value) => value !== null && value !== undefined && value !== ""
|
||||
const hasValidNumber = (value) => hasAmount(value) && Number.isFinite(Number(value))
|
||||
const isDepreciationItem = (item) => isDepreciationBookingMode(item?.bookingMode)
|
||||
|
||||
const updateBookingMode = (item) => {
|
||||
ensureDepreciationDefaults(item, itemInfo.value.date)
|
||||
}
|
||||
|
||||
const recalculateItem = (item, source) => {
|
||||
const taxRate = Number(taxOptions.value.find(i => i.key === item.taxType)?.percentage || 0);
|
||||
@@ -186,6 +200,7 @@ const updateIncomingInvoice = async (setBooked = false) => {
|
||||
}
|
||||
|
||||
let item = { ...itemInfo.value }
|
||||
item.accounts = (item.accounts || []).map((account) => ensureDepreciationDefaults({ ...account }, item.date))
|
||||
delete item.files
|
||||
item.state = setBooked ? "Gebucht" : "Entwurf"
|
||||
|
||||
@@ -205,6 +220,7 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
if(!i.accounts || i.accounts.length === 0) errors.push({message: "Es ist keine Position vorhanden", type: "breaking"})
|
||||
|
||||
i.accounts.forEach((account, idx) => {
|
||||
ensureDepreciationDefaults(account, i.date)
|
||||
if(!account.account) errors.push({message: `Pos ${idx+1}: Keine Kategorie`, type: "breaking"})
|
||||
if(!hasValidNumber(account.amountNet) && !hasValidNumber(account.amountGross)) {
|
||||
errors.push({message: `Pos ${idx+1}: Kein gültiger Betrag`, type: "breaking"})
|
||||
@@ -213,6 +229,12 @@ const findIncomingInvoiceErrors = computed(() => {
|
||||
if(hasValidNumber(account.amountNet) && !hasValidNumber(account.amountTax)) {
|
||||
errors.push({message: `Pos ${idx+1}: Steuerbetrag fehlt, bitte Steuer neu berechnen`, type: "warning"})
|
||||
}
|
||||
if(isDepreciationItem(account) && !Number(account.depreciationMonths)) {
|
||||
errors.push({message: `Pos ${idx+1}: Abschreibungsdauer fehlt`, type: "breaking"})
|
||||
}
|
||||
if(account.bookingMode === "depreciation_bundle" && !String(account.depreciationGroup || "").trim()) {
|
||||
errors.push({message: `Pos ${idx+1}: Sammelposten benötigt einen Gruppennamen`, type: "breaking"})
|
||||
}
|
||||
})
|
||||
|
||||
const order = { breaking: 0, warning: 1 }
|
||||
@@ -509,7 +531,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
value-key="id"
|
||||
:disabled="mode === 'show'"
|
||||
:filter-fields="['label', 'number']"
|
||||
:color="item.account ? 'primary' : 'error'"
|
||||
:color="item.account ? 'primary' : 'error'"
|
||||
>
|
||||
<template #item="{ item: option }">
|
||||
<span class="font-mono text-xs text-gray-500 mr-2">{{ option.number }}</span> {{ option.label }}
|
||||
@@ -521,6 +543,20 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<UFormField label="Aufwandsart">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
:items="EXPENSE_BOOKING_MODE_ITEMS"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
v-model="item.bookingMode"
|
||||
:disabled="mode === 'show'"
|
||||
@update:model-value="updateBookingMode(item)"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<UFormField label="Kostenstelle">
|
||||
<USelectMenu
|
||||
@@ -539,6 +575,68 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<template v-if="isDepreciationItem(item)">
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<UFormField label="Abschreibungsdauer (Monate)">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
v-model="item.depreciationMonths"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<UFormField label="Methode">
|
||||
<USelectMenu
|
||||
class="w-full"
|
||||
:items="DEPRECIATION_METHOD_ITEMS"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
v-model="item.depreciationMethod"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<UFormField label="Start Abschreibung">
|
||||
<UInput
|
||||
class="w-full"
|
||||
type="date"
|
||||
v-model="item.depreciationStartDate"
|
||||
:disabled="mode === 'show'"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-4">
|
||||
<UFormField :label="item.bookingMode === 'depreciation_bundle' ? 'Sammelposten' : 'Bezeichnung Abschreibung'">
|
||||
<UInput
|
||||
class="w-full"
|
||||
v-model="item[item.bookingMode === 'depreciation_bundle' ? 'depreciationGroup' : 'depreciationLabel']"
|
||||
:disabled="mode === 'show'"
|
||||
:placeholder="item.bookingMode === 'depreciation_bundle' ? 'z. B. IT-Hardware 2026' : 'z. B. Notebook Fuhrpark' "
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12">
|
||||
<UAlert
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-calendar-days"
|
||||
title="Nur die monatliche Abschreibung erscheint in der BWA"
|
||||
:description="item.bookingMode === 'depreciation_bundle'
|
||||
? 'Diese Position wird nicht sofort als Aufwand gezählt, sondern als Sammelposten periodisiert.'
|
||||
: 'Diese Position wird nicht sofort als Aufwand gezählt, sondern über die gewählte Laufzeit abgeschrieben.'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<UFormField label="Betrag (Netto)">
|
||||
<UInput
|
||||
@@ -624,7 +722,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
|
||||
icon="i-heroicons-plus"
|
||||
variant="soft"
|
||||
block
|
||||
@click="itemInfo.accounts.push({account:null, amountNet: null, amountTax:0, amountGross: null, taxType: '19'})"
|
||||
@click="itemInfo.accounts.push(createIncomingInvoiceAccount({ depreciationStartDate: itemInfo.date || null }))"
|
||||
>
|
||||
Weitere Position hinzufügen
|
||||
</UButton>
|
||||
|
||||
Reference in New Issue
Block a user