Added Abschreibungen

This commit is contained in:
2026-03-25 17:13:59 +01:00
parent f6c9875320
commit 42e0d7b35e
16 changed files with 13054 additions and 55 deletions

View File

@@ -6,10 +6,16 @@ import {secrets} from "../src/utils/secrets";
console.log("[DB INIT] 1. Suche Connection String..."); console.log("[DB INIT] 1. Suche Connection String...");
const fallbackConnectionString = "postgres://postgres:wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu@fedeo-db-001.vpn.internal:5432/fedeo"
// Checken woher die URL kommt // Checken woher die URL kommt
let connectionString = process.env.DATABASE_URL || secrets.DATABASE_URL; let connectionString = process.env.DATABASE_URL || secrets.DATABASE_URL || fallbackConnectionString;
if (connectionString) { if (process.env.DATABASE_URL) {
console.log("[DB INIT] -> Gefunden in process.env.DATABASE_URL"); console.log("[DB INIT] -> Gefunden in process.env.DATABASE_URL");
} else if (secrets.DATABASE_URL) {
console.log("[DB INIT] -> Gefunden in secrets.DATABASE_URL");
} else if (connectionString) {
console.log("[DB INIT] -> Nutze Fallback aus dem Projekt");
} else { } else {
console.error("[DB INIT] ❌ KEIN CONNECTION STRING GEFUNDEN! .env nicht geladen?"); console.error("[DB INIT] ❌ KEIN CONNECTION STRING GEFUNDEN! .env nicht geladen?");
} }
@@ -24,4 +30,4 @@ pool.query('SELECT NOW()')
.then(res => console.log(`[DB INIT] ✅ VERBINDUNG ERFOLGREICH! Zeit auf DB: ${res.rows[0].now}`)) .then(res => console.log(`[DB INIT] ✅ VERBINDUNG ERFOLGREICH! Zeit auf DB: ${res.rows[0].now}`))
.catch(err => console.error(`[DB INIT] ❌ VERBINDUNGSFEHLER:`, err.message)); .catch(err => console.error(`[DB INIT] ❌ VERBINDUNGSFEHLER:`, err.message));
export const db = drizzle(pool, { schema }); export const db = drizzle(pool, { schema });

View File

@@ -0,0 +1,6 @@
ALTER TABLE "statementallocations"
ADD COLUMN "booking_mode" text DEFAULT 'expense' NOT NULL,
ADD COLUMN "depreciation_months" integer,
ADD COLUMN "depreciation_start_date" text,
ADD COLUMN "depreciation_label" text,
ADD COLUMN "depreciation_group" text;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "statementallocations"
ADD COLUMN "depreciation_method" text,
ADD COLUMN "residual_value" double precision;

File diff suppressed because it is too large Load Diff

View File

@@ -138,37 +138,58 @@
{ {
"idx": 19, "idx": 19,
"version": "7", "version": "7",
"when": 1773489600000,
"tag": "0019_custom_surcharge_percentage_decimal",
"breakpoints": true
},
{
"idx": 20,
"version": "7",
"when": 1773572400000, "when": 1773572400000,
"tag": "0020_file_extracted_text", "tag": "0020_file_extracted_text",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 20, "idx": 21,
"version": "7", "version": "7",
"when": 1773835200000, "when": 1773835200000,
"tag": "0021_admin_user_flag", "tag": "0021_admin_user_flag",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 21, "idx": 22,
"version": "7", "version": "7",
"when": 1773925200000, "when": 1773925200000,
"tag": "0022_task_dependencies", "tag": "0022_task_dependencies",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 22, "idx": 23,
"version": "7", "version": "7",
"when": 1774080000000, "when": 1774080000000,
"tag": "0023_tax_evaluation_period", "tag": "0023_tax_evaluation_period",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 23, "idx": 24,
"version": "7", "version": "7",
"when": 1774393200000, "when": 1774393200000,
"tag": "0024_tenant_branches", "tag": "0024_tenant_branches",
"breakpoints": true "breakpoints": true
},
{
"idx": 25,
"version": "7",
"when": 1774393201000,
"tag": "0025_statementallocation_depreciation",
"breakpoints": true
},
{
"idx": 26,
"version": "7",
"when": 1774393202000,
"tag": "0026_statementallocation_depreciation_method",
"breakpoints": true
} }
] ]
} }

View File

@@ -51,6 +51,14 @@ export const statementallocations = pgTable("statementallocations", {
description: text("description"), description: text("description"),
bookingMode: text("booking_mode").notNull().default("expense"),
depreciationMonths: integer("depreciation_months"),
depreciationStartDate: text("depreciation_start_date"),
depreciationMethod: text("depreciation_method"),
depreciationLabel: text("depreciation_label"),
depreciationGroup: text("depreciation_group"),
residualValue: doublePrecision("residual_value"),
customer: bigint("customer", { mode: "number" }).references( customer: bigint("customer", { mode: "number" }).references(
() => customers.id () => customers.id
), ),

View File

@@ -1,11 +1,14 @@
import "dotenv/config"
import { defineConfig } from "drizzle-kit" import { defineConfig } from "drizzle-kit"
import {secrets} from "./src/utils/secrets";
const fallbackDatabaseUrl = "postgres://postgres:wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu@fedeo-db-001.vpn.internal:5432/fedeo"
const databaseUrl = process.env.DATABASE_URL || fallbackDatabaseUrl
export default defineConfig({ export default defineConfig({
dialect: "postgresql", dialect: "postgresql",
schema: "./db/schema", schema: "./db/schema",
out: "./db/migrations", out: "./db/migrations",
dbCredentials: { dbCredentials: {
url: secrets.DATABASE_URL || "postgres://postgres:wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu@fedeo-db-001.vpn.internal:5432/fedeo", url: databaseUrl,
}, },
}) })

View File

@@ -5,6 +5,7 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts", "dev": "tsx watch src/index.ts",
"migrate": "tsx scripts/migrate.ts",
"fill": "ts-node src/webdav/fill-file-sizes.ts", "fill": "ts-node src/webdav/fill-file-sizes.ts",
"dev:dav": "tsx watch src/webdav/server.ts", "dev:dav": "tsx watch src/webdav/server.ts",
"build": "tsc", "build": "tsc",

View File

@@ -52,6 +52,7 @@ export default async function meRoutes(server: FastifyInstance) {
id: tenants.id, id: tenants.id,
name: tenants.name, name: tenants.name,
short: tenants.short, short: tenants.short,
hasActiveLicense: tenants.hasActiveLicense,
locked: tenants.locked, locked: tenants.locked,
features: tenants.features, features: tenants.features,
extraModules: tenants.extraModules, extraModules: tenants.extraModules,

View File

@@ -136,6 +136,11 @@ const links = computed(() => {
to: "/incomingInvoices", to: "/incomingInvoices",
icon: "i-heroicons-document-text", icon: "i-heroicons-document-text",
} : null, } : 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")) ? { ((featureEnabled("createDocument") || featureEnabled("incomingInvoices")) || featureEnabled("accounts") || featureEnabled("ownaccounts") || featureEnabled("costcentres")) ? {
label: "Auswertungen", label: "Auswertungen",
icon: "i-heroicons-chart-pie", icon: "i-heroicons-chart-pie",

View File

@@ -4,16 +4,24 @@ import {
getCreatedDocumentTaxBreakdown, getCreatedDocumentTaxBreakdown,
getIncomingInvoiceTaxBreakdown getIncomingInvoiceTaxBreakdown
} from "~/composables/useTaxEvaluation" } from "~/composables/useTaxEvaluation"
import {
getIncomingInvoiceDepreciationRows,
getIncomingInvoiceImmediateExpenseNet,
getStatementAllocationDepreciationRow,
getStatementAllocationImmediateExpenseAmount
} from "~/composables/useDepreciation"
const loading = ref(true) const loading = ref(true)
const summary = ref({ const summary = ref({
label: "", label: "",
income: 0, income: 0,
expenses: 0, expenses: 0,
depreciations: 0,
result: 0, result: 0,
taxBalance: 0, taxBalance: 0,
incomeCount: 0, incomeCount: 0,
expenseCount: 0 expenseCount: 0,
depreciationCount: 0
}) })
const formatCurrency = (value: number) => { const formatCurrency = (value: number) => {
@@ -91,13 +99,20 @@ const loadSummary = async () => {
}, 0) }, 0)
const invoiceExpenses = inputDocs.reduce((sum: number, invoice: any) => { 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) }, 0)
const directAccountExpenses = directExpenses.reduce((sum: number, allocation: any) => { const directAccountExpenses = directExpenses.reduce((sum: number, allocation: any) => {
return sum + Math.abs(Number(allocation.amount || 0)) return sum + getStatementAllocationImmediateExpenseAmount(allocation)
}, 0) }, 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 outputTax = outputDocs.reduce((sum: number, doc: any) => {
const breakdown = getCreatedDocumentTaxBreakdown(doc) const breakdown = getCreatedDocumentTaxBreakdown(doc)
return sum + breakdown.tax19 + breakdown.tax7 return sum + breakdown.tax19 + breakdown.tax7
@@ -108,16 +123,18 @@ const loadSummary = async () => {
return sum + breakdown.tax19 + breakdown.tax7 return sum + breakdown.tax19 + breakdown.tax7
}, 0) }, 0)
const expenses = invoiceExpenses + directAccountExpenses const expenses = invoiceExpenses + directAccountExpenses + depreciations
summary.value = { summary.value = {
label: dayjs().format("MMMM YYYY"), label: dayjs().format("MMMM YYYY"),
income: Number(income.toFixed(2)), income: Number(income.toFixed(2)),
expenses: Number(expenses.toFixed(2)), expenses: Number(expenses.toFixed(2)),
depreciations: Number(depreciations.toFixed(2)),
result: Number((income - expenses).toFixed(2)), result: Number((income - expenses).toFixed(2)),
taxBalance: Number((outputTax - inputTax).toFixed(2)), taxBalance: Number((outputTax - inputTax).toFixed(2)),
incomeCount: outputDocs.length, 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 { } finally {
loading.value = false loading.value = false
@@ -159,6 +176,13 @@ onMounted(loadSummary)
</span> </span>
</div> </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"> <div class="bwa-summary-row">
<span class="bwa-summary-label">Ergebnis</span> <span class="bwa-summary-label">Ergebnis</span>
<span class="bwa-summary-value" :class="summary.result >= 0 ? 'text-primary-500' : 'text-error'"> <span class="bwa-summary-value" :class="summary.result >= 0 ? 'text-primary-500' : 'text-error'">
@@ -167,7 +191,7 @@ onMounted(loadSummary)
</div> </div>
<div class="bwa-summary-meta"> <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>
</div> </div>
</template> </template>

View 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,
}
}

View File

@@ -4,6 +4,15 @@ import {
getCreatedDocumentTaxBreakdown, getCreatedDocumentTaxBreakdown,
getIncomingInvoiceTaxBreakdown getIncomingInvoiceTaxBreakdown
} from "~/composables/useTaxEvaluation" } from "~/composables/useTaxEvaluation"
import {
getIncomingInvoiceDepreciationRows,
getIncomingInvoiceImmediateExpenseGross,
getIncomingInvoiceImmediateExpenseNet,
isDepreciationBookingMode,
normalizeIncomingInvoiceAccount,
getStatementAllocationDepreciationRow,
getStatementAllocationImmediateExpenseAmount
} from "~/composables/useDepreciation"
const router = useRouter() const router = useRouter()
@@ -51,6 +60,14 @@ const ownAccountColumns = [
{ accessorKey: "bookings", header: "Buchungen" } { 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) => { const isRelevantOutputDocument = (doc: any) => {
return doc?.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type) return doc?.state === "Gebucht" && ["invoices", "advanceInvoices", "cancellationInvoices"].includes(doc?.type)
} }
@@ -97,16 +114,6 @@ const computeDocumentNet = (doc: any) => {
}, 0).toFixed(2)) }, 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 yearItems = computed(() => {
const years = new Set<string>([String(dayjs().year())]) 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) 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(() => { const incomeTotal = computed(() => {
return Number(filteredDocuments.value.reduce((sum, doc) => sum + computeDocumentNet(doc), 0).toFixed(2)) return Number(filteredDocuments.value.reduce((sum, doc) => sum + computeDocumentNet(doc), 0).toFixed(2))
}) })
const expenseNetTotal = computed(() => { const expenseNetTotal = computed(() => {
const invoiceExpenses = filteredIncomingInvoices.value.reduce((sum, invoice) => { 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) }, 0)
const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => { const directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
const amount = Number(allocation.amount || 0) return sum + getStatementAllocationImmediateExpenseAmount(allocation)
return amount < 0 ? sum + Math.abs(amount) : sum
}, 0) }, 0)
return Number((invoiceExpenses + directAccountExpenses).toFixed(2)) const depreciations = depreciationTotal.value
return Number((invoiceExpenses + directAccountExpenses + depreciations).toFixed(2))
}) })
const expenseGrossTotal = computed(() => { 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 directAccountExpenses = filteredAccountStatementAllocations.value.reduce((sum, allocation) => {
const amount = Number(allocation.amount || 0) return sum + getStatementAllocationImmediateExpenseAmount(allocation)
return amount < 0 ? sum + Math.abs(amount) : sum
}, 0) }, 0)
return Number((invoiceExpenses + directAccountExpenses).toFixed(2)) const depreciations = depreciationTotal.value
return Number((invoiceExpenses + directAccountExpenses + depreciations).toFixed(2))
}) })
const taxSummary = computed(() => { const taxSummary = computed(() => {
@@ -221,7 +242,42 @@ const operatingResult = computed(() => {
const incomeDocumentCount = computed(() => filteredDocuments.value.length) const incomeDocumentCount = computed(() => filteredDocuments.value.length)
const expenseDocumentCount = computed(() => { 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(() => { const accountRows = computed(() => {
@@ -229,6 +285,7 @@ const accountRows = computed(() => {
.map((account) => { .map((account) => {
const invoiceBookings = filteredIncomingInvoices.value.flatMap((invoice) => { const invoiceBookings = filteredIncomingInvoices.value.flatMap((invoice) => {
return (invoice.accounts || []) return (invoice.accounts || [])
.filter((invoiceAccount: any) => !isDepreciationBookingMode(normalizeIncomingInvoiceAccount(invoiceAccount, invoice?.date).bookingMode))
.filter((invoiceAccount: any) => sameId(invoiceAccount.account?.id || invoiceAccount.account, account.id)) .filter((invoiceAccount: any) => sameId(invoiceAccount.account?.id || invoiceAccount.account, account.id))
.map((invoiceAccount: any) => ({ .map((invoiceAccount: any) => ({
type: "incominginvoice", type: "incominginvoice",
@@ -241,6 +298,7 @@ const accountRows = computed(() => {
}) })
const directBookings = filteredAccountStatementAllocations.value const directBookings = filteredAccountStatementAllocations.value
.filter((allocation) => getStatementAllocationImmediateExpenseAmount(allocation) > 0)
.filter((allocation) => sameId(allocation.account?.id || allocation.account, account.id)) .filter((allocation) => sameId(allocation.account?.id || allocation.account, account.id))
.map((allocation) => { .map((allocation) => {
const amount = Number(allocation.amount || 0) const amount = Number(allocation.amount || 0)
@@ -387,7 +445,7 @@ onMounted(setupPage)
</UFormField> </UFormField>
</div> </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"> <UCard class="min-w-0">
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen netto</div> <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> <div class="mt-2 text-2xl font-semibold">{{ useCurrency(incomeTotal) }}</div>
@@ -398,12 +456,20 @@ onMounted(setupPage)
<UCard class="min-w-0"> <UCard class="min-w-0">
<div class="text-sm text-gray-500 dark:text-gray-400">Ausgaben netto</div> <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"> <div class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Brutto: {{ useCurrency(expenseGrossTotal) }} Brutto: {{ useCurrency(expenseGrossTotal) }}
</div> </div>
</UCard> </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"> <UCard class="min-w-0">
<div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen Belege</div> <div class="text-sm text-gray-500 dark:text-gray-400">Einnahmen Belege</div>
<div class="mt-2 text-2xl font-semibold">{{ incomeDocumentCount }}</div> <div class="mt-2 text-2xl font-semibold">{{ incomeDocumentCount }}</div>
@@ -503,10 +569,10 @@ onMounted(setupPage)
</UCard> </UCard>
</div> </div>
<div class="grid min-w-0 gap-4 xl:grid-cols-2"> <div class="grid min-w-0 gap-4 xl:grid-cols-2">
<UCard class="min-w-0"> <UCard class="min-w-0">
<template #header> <template #header>
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<span class="font-semibold">Buchungskonten</span> <span class="font-semibold">Buchungskonten</span>
<UBadge color="neutral" variant="soft">{{ accountRows.length }}</UBadge> <UBadge color="neutral" variant="soft">{{ accountRows.length }}</UBadge>
</div> </div>
@@ -594,7 +660,26 @@ onMounted(setupPage)
</template> </template>
</UTable> </UTable>
</div> </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> </UCard>
</div>
</UDashboardPanelContent> </UDashboardPanelContent>
</template> </template>

View 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>

View File

@@ -1,5 +1,11 @@
<script setup> <script setup>
import dayjs from "dayjs"; 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 // import {filter} from "vuedraggable/dist/vuedraggable.common.js"; // Scheint nicht genutzt zu werden, auskommentiert
defineShortcuts({ defineShortcuts({
@@ -43,6 +49,7 @@ const setup = async () => {
if (itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value)) if (itemInfo.value) oldItemInfo.value = JSON.parse(JSON.stringify(itemInfo.value))
manualAllocationSum.value = calculateOpenSum.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)")) createddocuments.value = (await useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"))
const documents = createddocuments.value.filter(i => i.type === "invoices" || i.type === "advanceInvoices") 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 manualAllocationSum = ref(itemInfo.value.amount || 0)
const allocationDescription = ref("") 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 showMoreWithoutRecipe = ref(false)
const showMoreText = 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 saveAllocation = async (allocation) => {
const res = await useNuxtApp().$api("/api/banking/statements", { const res = await useNuxtApp().$api("/api/banking/statements", {
method: "POST", method: "POST",
body: {data: allocation} body: {data: buildAllocationPayload(allocation)}
}) })
if (res) { if (res) {
@@ -144,6 +194,13 @@ const saveAllocation = async (allocation) => {
vendorAccountToSave.value = null vendorAccountToSave.value = null
customerAccountToSave.value = null customerAccountToSave.value = null
ownAccountToSave.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? // allocationDescription.value = null // Optional: Beschreibung behalten für nächste Buchung?
} }
} }
@@ -420,6 +477,13 @@ const getAllocationIcon = (item) => {
return 'i-heroicons-question-mark-circle' 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() setup()
</script> </script>
@@ -526,6 +590,12 @@ setup()
{{ getAllocationLabel(item) }} {{ getAllocationLabel(item) }}
</div> </div>
<div class="text-xs text-gray-500 mt-0.5" v-if="item.description">{{ item.description }}</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"> <div class="flex gap-2 mt-1" v-if="item.createddocument || item.incominginvoice">
<UButton <UButton
v-if="item.createddocument" v-if="item.createddocument"
@@ -618,6 +688,56 @@ setup()
</div> </div>
</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" <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"> 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" <USelectMenu :items="ownaccounts" value-key="id" label-key="name" v-model="ownAccountToSave"

View File

@@ -3,6 +3,14 @@ import InputGroup from "~/components/InputGroup.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { parseDate } from "@internationalized/date" import { parseDate } from "@internationalized/date"
import { useDraggable } from '@vueuse/core' import { useDraggable } from '@vueuse/core'
import {
DEPRECIATION_METHOD_ITEMS,
EXPENSE_BOOKING_MODE_ITEMS,
createIncomingInvoiceAccount,
ensureDepreciationDefaults,
isDepreciationBookingMode,
normalizeIncomingInvoiceAccounts
} from "~/composables/useDepreciation"
// --- Standard Setup & Data --- // --- Standard Setup & Data ---
const dataStore = useDataStore() const dataStore = useDataStore()
@@ -30,14 +38,7 @@ const itemInfo = ref({
description: "", description: "",
state: "Entwurf", state: "Entwurf",
accounts: [ accounts: [
{ createIncomingInvoiceAccount()
account: null,
amountNet: null,
amountTax: null,
taxType: "19",
costCentre: null,
amountGross: null
}
] ]
}) })
@@ -61,12 +62,12 @@ const setup = async () => {
itemInfo.value = { itemInfo.value = {
...invoiceData, ...invoiceData,
vendor: invoiceData.vendor?.id || invoiceData.vendor, vendor: invoiceData.vendor?.id || invoiceData.vendor,
accounts: invoiceData.accounts || [] accounts: normalizeIncomingInvoiceAccounts(invoiceData.accounts || [], invoiceData.date)
} }
// Fallback Accounts // Fallback Accounts
if(itemInfo.value.accounts.length === 0) { 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 // Datei laden
@@ -92,6 +93,14 @@ const setup = async () => {
setup() setup()
watch(() => itemInfo.value.date, (value) => {
;(itemInfo.value.accounts || []).forEach((account) => {
if (isDepreciationItem(account) && !account.depreciationStartDate) {
account.depreciationStartDate = value || null
}
})
})
// --- Berechnungslogik --- // --- Berechnungslogik ---
const useNetMode = ref(false) const useNetMode = ref(false)
@@ -144,6 +153,11 @@ const totalCalculated = computed(() => {
const hasAmount = (value) => value !== null && value !== undefined && value !== "" const hasAmount = (value) => value !== null && value !== undefined && value !== ""
const hasValidNumber = (value) => hasAmount(value) && Number.isFinite(Number(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 recalculateItem = (item, source) => {
const taxRate = Number(taxOptions.value.find(i => i.key === item.taxType)?.percentage || 0); 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 } let item = { ...itemInfo.value }
item.accounts = (item.accounts || []).map((account) => ensureDepreciationDefaults({ ...account }, item.date))
delete item.files delete item.files
item.state = setBooked ? "Gebucht" : "Entwurf" 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"}) if(!i.accounts || i.accounts.length === 0) errors.push({message: "Es ist keine Position vorhanden", type: "breaking"})
i.accounts.forEach((account, idx) => { i.accounts.forEach((account, idx) => {
ensureDepreciationDefaults(account, i.date)
if(!account.account) errors.push({message: `Pos ${idx+1}: Keine Kategorie`, type: "breaking"}) if(!account.account) errors.push({message: `Pos ${idx+1}: Keine Kategorie`, type: "breaking"})
if(!hasValidNumber(account.amountNet) && !hasValidNumber(account.amountGross)) { if(!hasValidNumber(account.amountNet) && !hasValidNumber(account.amountGross)) {
errors.push({message: `Pos ${idx+1}: Kein gültiger Betrag`, type: "breaking"}) 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)) { if(hasValidNumber(account.amountNet) && !hasValidNumber(account.amountTax)) {
errors.push({message: `Pos ${idx+1}: Steuerbetrag fehlt, bitte Steuer neu berechnen`, type: "warning"}) 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 } const order = { breaking: 0, warning: 1 }
@@ -509,7 +531,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
value-key="id" value-key="id"
:disabled="mode === 'show'" :disabled="mode === 'show'"
:filter-fields="['label', 'number']" :filter-fields="['label', 'number']"
:color="item.account ? 'primary' : 'error'" :color="item.account ? 'primary' : 'error'"
> >
<template #item="{ item: option }"> <template #item="{ item: option }">
<span class="font-mono text-xs text-gray-500 mr-2">{{ option.number }}</span> {{ option.label }} <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> </UFormField>
</div> </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"> <div class="col-span-12 md:col-span-6">
<UFormField label="Kostenstelle"> <UFormField label="Kostenstelle">
<USelectMenu <USelectMenu
@@ -539,6 +575,68 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
</UFormField> </UFormField>
</div> </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"> <div class="col-span-12 md:col-span-3">
<UFormField label="Betrag (Netto)"> <UFormField label="Betrag (Netto)">
<UInput <UInput
@@ -624,7 +722,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
icon="i-heroicons-plus" icon="i-heroicons-plus"
variant="soft" variant="soft"
block 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 Weitere Position hinzufügen
</UButton> </UButton>