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 = {}) => ({ 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 = {}, 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, 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, } }