Added Abschreibungen
This commit is contained in:
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user