398 lines
16 KiB
TypeScript
398 lines
16 KiB
TypeScript
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 hasLinkedDocument = allocation?.incominginvoice || allocation?.createddocument || allocation?.ii_id || allocation?.cd_id
|
|
if (hasLinkedDocument) return 0
|
|
|
|
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,
|
|
}
|
|
}
|